Skip to content

《K8s 节点 CPU 异常与宿主机假死深度排查实战》

前言

教材定位与使用说明

本教材面向生产环境中的 Kubernetes 节点性能故障排查与预防,是一本从 Linux 内核底层机制出发、贯穿 K8s 全栈的实战型技术教材。

适用读者:Linux 运维工程师、SRE、K8s 平台工程师、云原生架构师。

前置知识要求

  • Linux 内核基础:进程管理、内存管理、文件系统、网络协议栈

  • K8s 核心概念:Pod、Node、CNI、Kubelet、Cgroup、Namespace

  • Shell 脚本基础与常用性能工具(top、vmstat、iostat)

教学方法:理论 30% + 实战 Lab 50% + 案例复盘 20%。每章遵循"故障注入 → 排查定位 → 修复优化 → 预防基线"闭环。

实验环境要求

  • K8s 1.28+(推荐 Cgroup v2)

  • Linux Kernel 5.15+(推荐 6.1 LTS)

  • 可观测栈:Prometheus + Grafana + Loki + Alertmanager

  • eBPF 工具链:BCC 0.29+、Cilium 1.15+、Parca

  • 至少 3 节点集群(1 控制面 + 2 工作节点),每节点 ≥4C/16G


第一章:K8s 节点 CPU 异常的认知革命

1.1 学习目标

完成本章学习后,你将能够:

  1. 解释为什么 K8s 监控面板的 CPU 使用率会"骗人",理解 CFS 时间片幻觉与 Throttling 假象

  2. 区分 Load Average、CPU Usage、CFS Throttling、D-State 四种指标的本质差异

  3. 掌握"先止血→后查因→先系统→后应用→先宏观→后微观"三阶排查法

  4. 建立正确的 K8s 节点性能排查心智模型

1.2 为什么 K8s 的 CPU 监控会"骗人"?

1.2.1 CFS 调度器的"时间片"幻觉

在传统的物理机或虚拟机上,CPU 使用率通常能真实反映系统的繁忙程度。但在 K8s 中,容器被分配了 requestslimits,Linux CFS(Completely Fair Scheduler,完全公平调度器)会根据 Cgroup 权重分配时间片。

核心机制:CFS 以 100ms(sched_latency_ns,默认 100000000ns)为一个调度周期,将 CPU 时间按权重分配给各 Cgroup。当容器触及 cpu.cfs_quota_us 上限时,该 Cgroup 下所有线程被强制挂起,直到下一个周期。

幻觉场景

假设一台 8 核节点上运行了一个绑核(CPU Affinity)的时延敏感型服务(绑定在 CPU 3 上),同时有一个非绑核的批处理容器。当批处理容器的线程被调度到 CPU 3 上时:

Plain
CPU 3 时间线:
|--- 延迟敏感服务线程 ---|--- 批处理线程 ---|--- 延迟敏感服务线程 ---|
                         ↑ 排队等待          ↑ 排队等待

此时:

  • 整体 CPU 使用率:可能只有 50%(因为其他核心空闲)

  • CPU 3 的 Run Queue:从 1 飙升到 3-5

  • 业务 RT(响应时间):从 2ms 飙升到 50ms+

  • Load Average:显著升高

监控面板告诉你"系统不忙",但业务已经在超时。

验证命令

Bash
# 查看每个 CPU 核心的运行队列长度
cat /proc/loadavg
# 输出:12.5 8.3 5.1 3/512 28456
# 第4个字段 3/512 表示:当前运行线程数/总线程数

# 查看特定 CPU 核心的调度统计
cat /proc/schedstat
# 或使用 perf sched latency 查看特定进程的调度延迟

# 查看 Cgroup 的 CPU 统计
cat /sys/fs/cgroup/kubepods.slice/kubepods-pod<UID>.slice/cpu.stat
# 关注:nr_periods, nr_throttled, throttled_usec

1.2.2 Throttling(限流)带来的"假象"

当容器触及 CPU Limits 时,CFS 会强制暂停该容器内的所有进程。cAdvisor 采集的 container_cpu_usage_seconds_total 可能显示该容器 CPU 使用率达到了 100%(即达到了 Limits 上限),但实际上:

  • 应用内部的线程因为被频繁挂起而处于饥饿状态

  • 业务请求在排队等待,超时率飙升

  • 这种"高使用率"掩盖了应用正在被系统严重限流的真相

关键指标对比

验证命令

Bash
# 找到目标 Pod 的 Cgroup 路径
POD_UID=$(kubectl get pod <pod-name> -o jsonpath='{.metadata.uid}')
CGROUP_PATH="/sys/fs/cgroup/kubepods.slice/kubepods-pod${POD_UID}.slice"

# Cgroup v2 查看限流统计
cat ${CGROUP_PATH}/cpu.stat
# 输出示例:
# usage_usec 45230000
# user_usec 38100000
# system_usec 7130000
# nr_periods 15230
# nr_throttled 892        ← 被限流 892 个周期!
# throttled_usec 4560000  ← 累计被限流 4.56 秒

# 计算限流比例
echo "scale=2; 892/15230*100" | bc
# 输出:5.85(%)

1.2.3 四种关键指标的本质区别

1.3 宿主机假死与监控断连的元凶分类

当节点出现"假死"(SSH 登录缓慢、命令无响应)且监控数据断断续续时,问题已深入内核态或硬件层。用户态的监控 Agent(如 Node Exporter)因无法获取 CPU 时间片或陷入 D 状态而停止上报。

1.3.1 元凶分类矩阵

1.3.2 D 状态进程与 I/O 阻塞

Linux 进程在等待磁盘 I/O 完成时会进入 D 状态(TASK_UNINTERRUPTIBLE),此时进程不可被 kill -9 终止。如果底层存储(云盘、NAS、CSI Volume)出现严重延迟,或文件系统元数据操作卡死,会导致大量进程堆积在 D 状态。

Bash
# 快速统计 D 状态进程数
ps -eo state | grep -c D

# 查看 D 状态进程详情及其等待的内核函数
ps -eo pid,stat,wchan:32,comm | grep " D"
# 输出示例:
# 12345 D    io_schedule              containerd-shim
# 12678 D    nfs4_call_sync_sequence  java
# 12901 D    blk_mq_get_tag           python3

# 查看 I/O 等待的内核栈
cat /proc/12345/stack
# 输出示例:
# [<0>] io_schedule+0x16/0x40
# [<0>] blk_mq_get_tag+0x1a2/0x280
# [<0>] blk_mq_alloc_request+0x8f/0x1e0

1.3.3 软中断(SoftIRQ)风暴

网络流量突增或负载均衡策略不当,可能导致某个 CPU 核心的软中断处理不过来。此时 ksoftirqd/N 进程占用 100% CPU。由于软中断优先级极高,它会完全剥夺用户态进程的执行时间。

Bash
# 查看各 CPU 核心的中断分布
mpstat -P ALL 1 3
# 关注 %soft 列,如果某核持续 >80% 即为异常

# 查看中断亲和性
cat /proc/interrupts | head -5
cat /proc/irq/<IRQ_NUM>/smp_affinity_list

# 查看软中断统计
cat /proc/softirqs
# NET_RX 和 NET_TX 是最常见的网络软中断

1.3.4 内核态锁竞争与 Soft Lockup

某些内核 Bug(如特定版本的 cgroup 内存回收死锁、conntrack 表锁竞争)或硬件驱动缺陷,可能导致 CPU 陷入死循环。

Bash
# 检查内核日志中的 Soft Lockup
dmesg -T | grep -i "soft lockup"
# 输出示例:
# [Mon Jun  9 03:22:15 2025] watchdog: BUG: soft lockup - CPU#3 stuck for 23s! [kworker/3:1:12345]

# 检查 RCU stall(另一种内核卡死表现)
dmesg -T | grep -i "rcu_sched self-detected stall"

1.4 运维视角的排查黄金法则

法则一:先止血,后查因

在生产环境中,业务可用性高于一切。在开始深度排查前,如果业务已经受损,应第一时间采取止血动作:

Bash
# 第一步:隔离故障节点(禁止新 Pod 调度)
kubectl cordon <node-name>

# 第二步:驱逐现有 Pod(优雅迁移)
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data --timeout=300s

# 第三步:网关层限流/降级(如果 Pod 迁移需要时间)
# 以 Nginx Ingress 为例
kubectl annotate ingress <ingress-name> \
  nginx.ingress.kubernetes.io/limit-rps="100" \
  nginx.ingress.kubernetes.io/limit-connections="50"

止血有效性验证(三项必查)

  1. ✅ 热点核心利用率下降到 ≤75%

  2. ✅ 业务 RT 在 3-5 分钟内回落到基线

  3. ✅ TCP 重传率和超时率同步下降

法则二:先系统,后应用

严禁一上来就分析业务代码火焰图。必须先确认操作系统层面是否存在瓶颈:

Bash
# 系统态快速画像(30秒内完成)
top -bn1 | head -20          # 整体 CPU/内存/进程概览
mpstat -P ALL 1 3            # 核级 CPU 分布(%usr/%sys/%soft/%wa/%steal)
vmstat 1 5                   # 上下文切换/中断/运行队列
iostat -xz 1 3               # 磁盘 I/O 饱和度
ss -s                        # 网络连接状态统计

判断逻辑

  • %sys > 30% → 内核态瓶颈(第二章)

  • %wa > 20% → I/O 阻塞(第二章)

  • %soft > 50% → 软中断风暴(第二章)

  • %steal > 10% → 虚拟化争抢(第三章)

  • 以上均正常 → 再进入应用层分析(第四章)

法则三:先宏观,后微观

先通过 topvmstatsar 观察整体资源画像,锁定瓶颈方向后,再使用 perfeBPF 进行精准定位。

严禁行为

  • 🚫 在假死节点上直接运行 perf record -F 999(高频采样会加剧负载)

  • 🚫 在 CPU 100% 的节点上运行 strace -p <PID>(ptrace 停顿可能成为压垮骆驼的最后一根稻草)

  • 🚫 在不确定方向时同时挂载多个重量级工具

1.5 实战 Lab

Lab 1.1:CFS 时间片幻觉复现

目标:观察绑核服务被非绑核线程干扰时,监控面板与实际延迟的背离。

Bash
# 步骤1:创建绑核的延迟敏感服务(模拟)
# 在节点上运行一个绑定到 CPU 3 的循环程序
taskset -c 3 sh -c 'while true; do :; done' &
SENSITIVE_PID=$!

# 步骤2:创建非绑核的批处理负载
stress-ng --cpu 4 --cpu-method matrixprod --timeout 60s &

# 步骤3:观察 CPU 3 的运行队列
watch -n 0.5 'cat /proc/schedstat | awk "NR==4{print}"'
# 第4行对应 CPU 3

# 步骤4:测量延迟敏感服务的调度延迟
perf sched record -p $SENSITIVE_PID -- sleep 10
perf sched latency -p $SENSITIVE_PID

# 步骤5:对比整体 CPU 使用率
mpstat -P ALL 1 10
# 观察:整体 Usage 可能只有 50%,但 CPU 3 的 %sys 和调度延迟极高

# 清理
kill $SENSITIVE_PID

Lab 1.2:CFS Throttling 触发与观测

目标:触发容器 CPU Throttling,对比 cpu.stat 与 cAdvisor 指标差异。

YAML
# throttle-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: throttle-test
spec:
  containers:
  - name: stress
    image: polinux/stress
    command: ["stress", "--cpu", "4", "--timeout", "300"]
    resources:
      requests:
        cpu: "500m"
      limits:
        cpu: "1"    # 限制为1核,但stress启动4个线程
  restartPolicy: Never
Bash
# 部署
kubectl apply -f throttle-test.yaml

# 等待 Pod 运行后,查看 Cgroup 限流统计
POD_UID=$(kubectl get pod throttle-test -o jsonpath='{.metadata.uid}')
CONTAINER_ID=$(crictl ps --name stress -q)
CGROUP_PATH=$(crictl inspect $CONTAINER_ID | jq -r '.info.runtimeSpec.linux.cgroupsPath')

# Cgroup v2
cat /sys/fs/cgroup/${CGROUP_PATH}/cpu.stat
# 观察 nr_throttled 持续增长

# 同时在 Prometheus 中查询
# container_cpu_cfs_throttled_periods_total{pod="throttle-test"}
# 对比 cpu.stat 中的 nr_periods 和 nr_throttled

1.6 最佳实践 Checklist

  • [ ] 禁止仅凭 CPU Usage 判断节点健康,必须联合 Load、Throttling、D-State、Steal 四维评估

  • [ ] 止血动作必须包含:cordon + drain + 网关限流,且验证 3 项恢复指标

  • [ ] 严禁在假死节点直接挂载 perf/strace 等重量级工具

  • [ ] 排查顺序严格遵循:系统态 → 虚拟化层 → 容器运行时 → 应用层

  • [ ] 每次排查必须记录时间线、使用的命令、观察到的数据,形成 RCA 素材

1.7 常见误区

1.8 思考题

  1. 如果你的节点有 64 个 CPU 核心,Load Average 为 80,但 CPU Usage 只有 40%,可能的原因有哪些?你会如何进一步排查?

  2. 一个 Java 应用设置了 CPU Limit = 2,监控显示 CPU 使用率持续在 1.8 左右,但业务 P99 延迟从 10ms 飙升到 500ms。请分析可能的原因并给出排查步骤。

  3. 为什么在 K8s 环境中,传统的"CPU 使用率 > 80% 告警"策略是不足的?你会设计怎样的多维告警规则?


第二章:系统态开销与内核瓶颈剖析

2.1 学习目标

完成本章学习后,你将能够:

  1. 使用 USE 方法(Utilization / Saturation / Errors)系统化定位系统态瓶颈

  2. 掌握软中断绑定、I/O 队列、conntrack 表溢出的完整排查路径

  3. 区分 %sys、%wa、%soft 三大系统态开销的根因并实施修复

  4. 配置 IRQ Affinity、RPS/RFS 优化网络中断处理

2.2 USE 方法论框架

在深入具体场景前,先建立系统化的排查框架。USE 方法由性能分析大师 Brendan Gregg 提出:

排查顺序:对每种资源(CPU、内存、磁盘、网络),依次检查 U → S → E。

2.3 软中断(SoftIRQ)与 ksoftirqd 飙高排查

2.3.1 机制详解

在 K8s 集群中,网络流量呈现"高并发、小包多"的特征(微服务 gRPC 调用、Service Mesh Sidecar 通信、K8s API Server 心跳)。当网卡接收到大量数据包时:

Plain
网络包到达 → 硬件中断(Hard IRQ) → 触发软中断(NET_RX) → ksoftirqd 处理

                          NAPI poll → 协议栈解析 → socket 投递

Linux 内核将耗时的数据包处理逻辑交给软中断,由 ksoftirqd/N 内核线程执行。当某个核心的软中断处理速率跟不上包到达速率时,就会形成"软中断风暴"。

2.3.2 现象与根因

2.3.3 实战排查路径

Bash
# 第一步:确认中断分布
mpstat -P ALL 1 5
# 输出示例(异常):
# CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %idle
#   0    2.10    0.00    3.20    0.50    0.10   92.30    0.00    0.00   1.80  ← 异常!
#   1   15.20    0.00    5.10    0.30    0.05    0.20    0.00    0.00  79.15
#   2   14.80    0.00    4.90    0.40    0.08    0.15    0.00    0.00  79.67
#   3   15.50    0.00    5.30    0.20    0.06    0.18    0.00    0.00  78.76

# 第二步:查看中断亲和性
cat /proc/interrupts | grep -i eth
# 输出示例:
#            CPU0       CPU1       CPU2       CPU3
#  28:   89234567     123456     134567     145678   PCI-MSI  eth0-rx-0
#  29:     123456   89345678     134567     145678   PCI-MSI  eth0-rx-1
# ← 所有 rx-0 队列的中断都打到了 CPU0!

# 第三步:查看软中断统计
cat /proc/softirqs
# 关注 NET_RX 行的分布

# 第四步:查看网络包速率
sar -n DEV 1 5
# 关注 rxpck/s(每秒接收包数),如果 >100K 且集中在单核,即为小包洪峰

# 第五步:查看 softnet 队列溢出
cat /proc/net/softnet_stat
# 每行3个十六进制数:处理的包数 / 丢弃的包数 / 时间挤压次数
# 如果第二列(丢弃)非零,说明软中断处理不过来

2.3.4 修复方案:IRQ Affinity 与 RPS/RFS

Bash
# 方案一:手动设置 IRQ 亲和性(将中断分散到多个核心)
# 查看网卡 IRQ 号
grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'
# 假设 IRQ 号为 28, 29, 30, 31

# 将 4 个队列分别绑定到 4 个核心
echo 1 > /proc/irq/28/smp_affinity   # CPU 0
echo 2 > /proc/irq/29/smp_affinity   # CPU 1
echo 4 > /proc/irq/30/smp_affinity   # CPU 2
echo 8 > /proc/irq/31/smp_affinity   # CPU 3

# 方案二:使用 irqbalance 服务(自动平衡)
systemctl enable irqbalance
systemctl start irqbalance

# 方案三:配置 RPS(Receive Packet Steering)- 软件级多核分发
# 将 eth0 的 rx-0 队列的 RPS 掩码设为所有核心
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus
# "f" = 二进制 1111 = CPU 0-3

# 方案四:配置 RFS(Receive Flow Steering)- 按连接哈希分发
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

生产环境持久化配置(使用 udev 规则):

Bash
# /etc/udev/rules.d/99-irq-affinity.rules
ACTION=="add", KERNEL=="eth0", RUN+="/usr/local/bin/set-irq-affinity.sh"
Bash
#!/bin/bash
# /usr/local/bin/set-irq-affinity.sh
# 将网卡中断均匀分布到所有核心,避开 CPU 0(预留给系统)
IRQ_LIST=$(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':')
CPU_COUNT=$(nproc)
CPU_IDX=1  # 从 CPU 1 开始,避开 CPU 0

for irq in $IRQ_LIST; do
    echo $CPU_IDX > /proc/irq/$irq/smp_affinity_list
    CPU_IDX=$(( (CPU_IDX + 1) % CPU_COUNT ))
    [ $CPU_IDX -eq 0 ] && CPU_IDX=1
done

2.4 磁盘 I/O 等待(%wa)与内核阻塞

2.4.1 机制详解

CPU 异常有时只是表象,真正的瓶颈在于存储 I/O。当进程发起文件读写请求时,如果底层存储响应缓慢,CPU 进入 I/O 等待状态(%wa)。虽然 CPU 并没有在"计算",但大量进程排队等待 I/O 会导致 Load 飙升。

K8s 节点常见的 I/O 风暴源

  • containerd-shim:容器日志写入(尤其是未配置日志轮转时)

  • kubelet:镜像拉取、PLEG 状态同步

  • CSI 插件:云盘挂载/卸载操作

  • 应用容器:大量小文件写入(如 Java 应用的 GC 日志)

2.4.2 实战排查路径

Bash
# 第一步:确认 I/O 瓶颈
iostat -xz 1 5
# 输出示例(异常):
# Device   r/s     w/s    rkB/s    wkB/s  await  avgqu-sz  %util
# nvme0n1  1250    3800   50000    152000  45.2    228.5    99.8  ← 磁盘饱和!
# 
# 关键指标:
# %util > 90%:磁盘已饱和
# await > 20ms(SSD)或 > 50ms(HDD):I/O 延迟异常
# avgqu-sz > 32:队列严重堆积

# 第二步:定位 I/O 热点进程
iotop -oPa
# -o:只显示有 I/O 的进程
# -P:显示进程而非线程
# -a:累积模式

# 输出示例:
#   PID  PRIO  USER     DISK READ  DISK WRITE  TOTAL  COMMAND
# 12345  be/4  root      0.00 B/s  850.00 M/s  850M   containerd-shim -namespace k8s.io
# 12678  be/4  root      0.00 B/s  120.00 M/s  120M   java -jar app.jar

# 第三步:追踪 D 状态进程
ps -eo pid,stat,wchan:32,comm | awk '$2 ~ /D/'
# 输出示例:
# 12345 D    io_schedule              containerd-shim
# 12901 D    nfs4_call_sync_sequence  java

# 第四步:查看特定进程的 I/O 统计
cat /proc/12345/io
# 输出:
# rchar: 1234567890
# wchar: 9876543210
# read_bytes: 123456789
# write_bytes: 987654321
# cancelled_write_bytes: 0

# 第五步:使用 pidstat 追踪进程级 I/O
pidstat -d 1 5
# 显示每个进程的 kB_rd/s 和 kB_wr/s

2.4.3 K8s 特有的 I/O 问题

问题一:容器日志风暴

Bash
# 查看容器日志大小
du -sh /var/log/pods/*/*/*.log | sort -rh | head -10

# 查看 kubelet 日志轮转配置
cat /var/lib/kubelet/config.yaml | grep -A5 containerLog
# 推荐配置:
# containerLogMaxSize: "100Mi"
# containerLogMaxFiles: 5

问题二:镜像拉取 I/O 风暴

Bash
# 查看 containerd 的并发下载配置
cat /etc/containerd/config.toml | grep -A5 "plugins.\"io.containerd.grpc.v1.cri\""
# 推荐:max_concurrent_downloads = 3(默认)

# 查看当前镜像拉取进程
ps aux | grep "containerd.*pull"

问题三:CSI 云盘 IOPS 耗尽

Bash
# 查看云盘 IOPS 使用情况(以阿里云为例)
# 通过云监控 API 或 CLI 查询
aliyun ecs DescribeDiskMonitorData --DiskId d-xxx --Period 60

# 在节点上验证
iostat -xz 1 | grep vd  # 云盘设备通常为 vda/vdb

2.5 网络协议栈瓶颈与连接风暴

2.5.1 conntrack 表溢出

K8s 节点上的每个 Service 连接都会被 conntrack 表追踪。当短连接并发量突破阈值时:

Bash
# 查看 conntrack 表使用情况
conntrack -C
# 输出:262144(当前表项数)

sysctl net.netfilter.nf_conntrack_max
# 输出:net.netfilter.nf_conntrack_max = 262144

# 计算使用率
echo "scale=2; 262144/262144*100" | bc
# 100%!表已满!

# 查看 conntrack 统计(含丢弃计数)
conntrack -S
# 输出示例:
# cpu=0 found=0 invalid=12345 insert=0 insert_failed=0 drop=89012 ← drop 非零!
# early_drop=89012 ← 因表满而丢弃的连接数

# 查看内核日志中的 conntrack 溢出告警
dmesg -T | grep "nf_conntrack: table full"
# [Mon Jun  9 10:22:15 2025] nf_conntrack: nf_conntrack: table full, dropping packet

修复方案

Bash
# 临时扩容
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_buckets=262144

# 持久化
cat >> /etc/sysctl.d/99-k8s-network.conf << EOF
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_buckets = 262144
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
EOF
sysctl --system

2.5.2 iptables/IPVS 规则膨胀

在大规模集群中(>1000 Service),iptables 规则匹配成为性能瓶颈:

Bash
# 查看 iptables 规则数量
iptables -L -n -t nat | wc -l
# 如果 > 10000 行,每次数据包匹配都需要线性遍历

# 查看 IPVS 规则(如果使用 IPVS 模式)
ipvsadm -Ln | wc -l

# 使用 perf 验证协议栈热点
perf top -g -e cycles -- sleep 10
# 如果看到 ipt_do_table、nf_conntrack_in、nf_hook_slow 占据 >20% CPU
# 说明网络协议栈已成为瓶颈

优化路径

2.5.3 连接状态异常

Bash
# 使用 ss 查看连接状态分布(替代 netstat,性能更好)
ss -s
# 输出示例:
# Total: 45230
# TCP:   38920 (estab 12345, closed 18900, orphaned 234, timewait 15678)

# 查看 TIME_WAIT 堆积
ss -tan state time-wait | wc -l
# 如果 > 10000,考虑启用 tcp_tw_reuse

# 查看 CLOSE_WAIT 堆积(通常意味着应用未正确关闭连接)
ss -tan state close-wait | wc -l
# 如果持续增长,说明应用存在连接泄漏

# 查看 SYN 队列溢出
netstat -s | grep -i "syn"
# 关注 "SYNs to LISTEN sockets dropped" 和 "times the listen queue of a socket overflowed"

2.6 实战 Lab

Lab 2.1:软中断风暴模拟与修复

Bash
# 步骤1:使用 hping3 制造小包洪峰(在另一台机器上执行)
hping3 -S -p 80 --flood <target-node-ip>

# 步骤2:在目标节点观察
mpstat -P ALL 1
# 观察某核 %soft 飙升

# 步骤3:查看中断分布
watch -n 1 'cat /proc/interrupts | grep eth0'

# 步骤4:配置 RPS 分散负载
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 步骤5:验证修复效果
mpstat -P ALL 1
# %soft 应分散到多个核心,单核不再 100%

Lab 2.2:conntrack 表溢出复现

Bash
# 步骤1:缩小 conntrack 表(模拟溢出)
sysctl -w net.netfilter.nf_conntrack_max=1024

# 步骤2:制造大量短连接
for i in $(seq 1 2000); do
  curl -s -o /dev/null http://<service-ip>:80 &
done

# 步骤3:观察溢出
dmesg -T | tail -5
# 应看到 "nf_conntrack: table full, dropping packet"

conntrack -S | grep drop
# drop 计数应 > 0

# 步骤4:恢复并扩容
sysctl -w net.netfilter.nf_conntrack_max=1048576

2.7 生产 SOP:系统态快速巡检

Bash
#!/bin/bash
# system-health-check.sh - 系统态快速巡检(30秒内完成)
echo "=== CPU 核级分布 ==="
mpstat -P ALL 1 3

echo "=== 磁盘 I/O ==="
iostat -xz 1 3

echo "=== 网络连接状态 ==="
ss -s

echo "=== Conntrack 使用率 ==="
CURRENT=$(conntrack -C 2>/dev/null || echo 0)
MAX=$(sysctl -n net.netfilter.nf_conntrack_max)
echo "Current: $CURRENT / Max: $MAX ($(echo "scale=1; $CURRENT/$MAX*100" | bc)%)"

echo "=== D-State 进程 ==="
ps -eo pid,stat,wchan:32,comm | awk '$2 ~ /D/' | head -10

echo "=== 软中断溢出 ==="
cat /proc/net/softnet_stat | awk '{print "CPU"NR": processed="$1" dropped="$2" time_squeeze="$3}'

echo "=== 内核错误日志(最近5分钟)==="
dmesg -T | tail -20 | grep -iE "error|fail|oom|lockup|panic"

2.8 最佳实践 Checklist

  • [ ] 所有 K8s 节点必须配置 IRQ Affinity,避免中断集中在单核

  • [ ] 启用 RPS/RFS,将网络包处理分散到多核

  • [ ] conntrack 表大小设置为预期最大连接数的 2 倍,并配置告警(使用率 > 80%)

  • [ ] 容器日志必须配置轮转:containerLogMaxSize: 100MicontainerLogMaxFiles: 5

  • [ ] 大规模集群(>1000 Service)必须使用 IPVS 或 eBPF 模式替代 iptables

  • [ ] 禁止使用 netstat -an 排查大连接数问题(会卡死节点),使用 ss -s

2.9 常见误区

2.10 思考题

  1. 一个 K8s 节点运行了 200 个 Pod,每个 Pod 每秒发起 100 个短连接。请计算 conntrack 表的压力,并设计合理的 nf_conntrack_max 值。

  2. 你发现节点的 %sys 持续在 40% 以上,但 perf top 显示的热点函数是 copy_user_generic_unrolled。这意味着什么?你会如何进一步优化?

  3. 为什么在 K8s 节点上,containerd-shim 进程经常成为 I/O 风暴的源头?有哪些预防措施?


第三章:虚拟化与 Cgroup 资源争抢深度机制

3.1 学习目标

完成本章学习后,你将能够:

  1. 量化 %steal 对业务的影响并制定云实例选型策略

  2. 深入理解 Cgroup v1/v2 CFS 带宽控制机制,解决 Throttling 引发的 GC/探针超时

  3. 配置 Topology Manager 实现 NUMA 资源亲和

  4. 掌握 CPU Burst 技术的原理与部署方法

3.2 CPU Steal 时间:被 Hypervisor 偷走的算力

3.2.1 机制详解

在物理机上,CPU 时间完全属于你;但在虚拟机上,CPU 时间是你与"邻居"抢来的。%steal 代表了虚拟机(Guest OS)准备运行,但 Hypervisor 因资源被其他虚拟机占用而无法分配物理 CPU 时间片的百分比。

Plain
物理 CPU 时间线:
|--- VM-A(你的节点)---|--- VM-B(邻居)---|--- VM-A ---|--- VM-C ---|
                         ↑ 你的进程在等待     ↑ 恢复执行
                         这段时间 = Steal Time

3.2.2 影响量化

3.2.3 排查与应对

Bash
# 确认 Steal 时间
mpstat -P ALL 1 10
# 关注 %steal 列

# 使用 vmstat 观察整体
vmstat 1 10
# 第16列(st)即为 steal time

# 查看 Hypervisor 类型
systemd-detect-virt
# 输出:kvm / xen / vmware / none

# 查看 CPU 型号与虚拟化特性
lscpu | grep -i "hypervisor\|virtual"

应对策略

3.3 CFS 调度器与 CPU Throttling 深度机制

3.3.1 CFS 带宽控制原理

Linux CFS 通过两个参数控制 Cgroup 的 CPU 配额:

  • cpu.cfs_period_us(Cgroup v1)/ cpu.max** 的第二个值**(Cgroup v2):调度周期,默认 100000μs(100ms)

  • cpu.cfs_quota_us(Cgroup v1)/ cpu.max** 的第一个值**(Cgroup v2):每个周期内允许使用的 CPU 时间

Plain
示例:CPU Limit = 1 核
cpu.cfs_period_us = 100000  (100ms)
cpu.cfs_quota_us  = 100000  (100ms)
→ 每 100ms 内,该 Cgroup 最多使用 100ms CPU 时间

示例:CPU Limit = 0.5 核
cpu.cfs_quota_us  = 50000   (50ms)
→ 每 100ms 内,该 Cgroup 最多使用 50ms CPU 时间

关键问题:当配额用完时,Cgroup 下所有线程被强制挂起,包括:

  • 业务处理线程

  • GC 线程(Java/Go)

  • 健康检查响应线程

  • 日志写入线程

3.3.2 Java 应用的 GC 灾难

这是 K8s 环境中最具迷惑性的问题之一:

Plain
时间线:
|--- 业务线程运行 ---|--- GC 触发 ---|--- CFS 配额用完 ---|

                                   所有线程被挂起(包括 GC 线程)

                                   GC STW 时间从 50ms 延长到 500ms+

                                   K8s Liveness Probe 超时(默认 1s)

                                   Pod 被 Kill 并重启

                                   重启后再次触发 GC → 循环

验证方法

Bash
# 查看 Java 应用的 GC 日志
kubectl logs <pod-name> | grep -i "pause"
# 输出示例:
# [GC pause (G1 Evacuation Pause) (young), 0.5234567 secs]  ← 正常应 < 0.1s

# 查看 Cgroup 限流统计
cat /sys/fs/cgroup/kubepods.slice/.../cpu.stat
# nr_throttled: 1523    ← 被限流 1523 次
# throttled_usec: 8900000  ← 累计被限流 8.9 秒

# 关联 Pod 重启事件
kubectl describe pod <pod-name> | grep -A5 "Events"
# 输出:
# Warning  Unhealthy  2m  kubelet  Liveness probe failed: HTTP probe failed with statuscode: 503
# Normal   Killing    2m  kubelet  Container failed liveness probe, will be restarted

3.3.3 修复方案

方案一:调整 CPU Limit

YAML
# 对于延迟敏感型应用,建议:
resources:
  requests:
    cpu: "2"      # 保证调度资源
  limits:
    cpu: "4"      # 允许突发到 2 倍
# 或者干脆不设 CPU Limit(仅设 Requests)

方案二:启用 CPU Burst(Linux 5.14+)

CPU Burst 允许容器在空闲时积累"CPU 信用",在突发时使用,避免硬性截断。

Bash
# 检查内核是否支持
cat /sys/fs/cgroup/cpu.max.burst  # 如果文件存在则支持

# Kubelet 配置启用(K8s 1.28+)
# kubelet-config.yaml
featureGates:
  CPUBurst: true
YAML
# Pod 级别配置
apiVersion: v1
kind: Pod
metadata:
  annotations:
    cpu-burst.kubernetes.io/burst: "100000"  # 允许额外突发 100ms
spec:
  containers:
  - name: app
    resources:
      limits:
        cpu: "1"

方案三:调整 CFS 周期(谨慎使用)

Bash
# 将 CFS 周期从 100ms 缩短到 10ms(减少单次挂起时间)
# 注意:会增加调度开销
echo 10000 > /sys/fs/cgroup/kubepods.slice/cpu.cfs_period_us

3.3.4 Cgroup v1 vs v2 对比

Bash
# 查看当前 Cgroup 版本
stat -fc %T /sys/fs/cgroup/
# cgroup2fs = Cgroup v2
# tmpfs = Cgroup v1

# Cgroup v2 查看 CPU 限制
cat /sys/fs/cgroup/kubepods.slice/cpu.max
# 输出:100000 100000  (quota=100ms, period=100ms → 1核)

# Cgroup v2 查看压力指标(PSI)
cat /sys/fs/cgroup/kubepods.slice/cpu.pressure
# 输出:some avg10=5.23 avg60=3.12 avg300=2.45 total=123456789
# avg10=5.23 表示最近10秒内,有5.23%的时间至少一个任务因CPU不足而等待

3.4 NUMA 架构与内存带宽争抢

3.4.1 机制详解

在高性能计算或数据库类 K8s 节点上,物理 CPU 采用 NUMA(Non-Uniform Memory Access)架构:

Plain
┌─────────────────────────────────────────┐
│              物理服务器                   │
│  ┌──────────────┐  ┌──────────────┐     │
│  │  Socket 0    │  │  Socket 1    │     │
│  │  CPU 0-15   │  │  CPU 16-31   │     │
│  │  本地内存    │←→│  本地内存    │     │
│  │  (NUMA 0)   │QPI│  (NUMA 1)   │     │
│  └──────────────┘  └──────────────┘     │
│        ↑ 本地访问:~80ns                 │
│        ↑ 远程访问:~140ns(+75%延迟)    │
└─────────────────────────────────────────┘

3.4.2 排查方法

Bash
# 查看 NUMA 拓扑
numactl -H
# 输出示例:
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# node 0 size: 64000 MB
# node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
# node 1 size: 64000 MB
# node distances:
# node   0   1
#   0:  10  21    ← 本地访问距离10,远程访问距离21(+110%)
#   1:  21  10

# 查看 NUMA 内存访问统计
numastat -m
# 关注 numa_hit(本地命中)和 numa_miss(远程访问)
# 如果 numa_miss / (numa_hit + numa_miss) > 20%,说明跨 NUMA 访问严重

# 查看特定进程的 NUMA 内存分布
numastat -p <PID>

3.4.3 K8s NUMA 亲和配置

YAML
# KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cpuManagerPolicy: "static"           # 启用 CPU 绑核
topologyManagerPolicy: "single-numa-node"  # 强制单 NUMA 分配
topologyManagerScope: "pod"          # Pod 级别对齐
reservedSystemCPUs: "0,1"           # 预留给系统的 CPU
memoryManagerPolicy: "Static"        # 内存也按 NUMA 分配
YAML
# Pod 配置(必须 Requests = Limits 才能触发 CPU Manager)
apiVersion: v1
kind: Pod
metadata:
  name: numa-sensitive-app
spec:
  containers:
  - name: redis
    image: redis:7-alpine
    resources:
      requests:
        cpu: "4"
        memory: "8Gi"
      limits:
        cpu: "4"        # 必须等于 requests
        memory: "8Gi"   # 必须等于 requests

3.5 实战 Lab

Lab 3.1:CFS Throttling 导致 Java GC 超时

Bash
# 步骤1:部署 Java 应用(设置 1 核 Limit)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: java-throttle-test
spec:
  containers:
  - name: java
    image: openjdk:17-slim
    command: ["java", "-Xmx512m", "-XX:+UseG1GC", "-jar", "/app/app.jar"]
    resources:
      requests:
        cpu: "500m"
      limits:
        cpu: "1"
EOF

# 步骤2:注入突发流量
kubectl exec -it java-throttle-test -- ab -n 10000 -c 100 http://localhost:8080/api

# 步骤3:观察 Throttling
POD_UID=$(kubectl get pod java-throttle-test -o jsonpath='{.metadata.uid}')
watch -n 1 "cat /sys/fs/cgroup/kubepods.slice/kubepods-pod${POD_UID}.slice/cpu.stat"
# 观察 nr_throttled 快速增长

# 步骤4:观察 GC 日志
kubectl logs java-throttle-test | grep "pause"
# GC pause 时间应从正常的 20-50ms 飙升到 200-500ms

# 步骤5:移除 Limit 后对比
kubectl patch pod java-throttle-test --type json \
  -p='[{"op":"remove","path":"/spec/containers/0/resources/limits/cpu"}]'
# 注意:需要重建 Pod 才能生效

Lab 3.2:NUMA 跨节点访问性能对比

Bash
# 步骤1:查看 NUMA 拓扑
numactl -H

# 步骤2:本地 NUMA 运行 Redis
numactl --cpunodebind=0 --membind=0 redis-server --port 6379 &

# 步骤3:跨 NUMA 运行 Redis
numactl --cpunodebind=0 --membind=1 redis-server --port 6380 &

# 步骤4:性能对比
redis-benchmark -p 6379 -t set,get -n 100000 -c 50  # 本地
redis-benchmark -p 6380 -t set,get -n 100000 -c 50  # 跨 NUMA
# 对比 P99 延迟差异(通常 20-40%)

3.6 最佳实践 Checklist

  • [ ] 延迟敏感型服务使用独占型实例或专有宿主机,消除 Steal

  • [ ] Java/Go 应用的 CPU Limit 至少为 Requests 的 2 倍,或不设 Limit

  • [ ] 启用 CPU Burst(内核 5.14+),减少突发流量下的 Throttling

  • [ ] 高性能应用(Redis/ES/DB)必须配置 NUMA 亲和(Topology Manager + CPU Manager)

  • [ ] Java 应用 Memory Limit ≥ Heap × 1.5,预留 Metaspace/线程栈/Native 内存

  • [ ] 使用 PSI(Pressure Stall Information)替代传统指标评估资源压力

3.7 常见误区

3.8 思考题

  1. 一个 Java 应用设置了 CPU Limit = 2,JVM 参数 -XX:ActiveProcessorCount=4。这会导致什么问题?如何修复?

  2. 在 Cgroup v2 环境下,cpu.pressure 显示 some avg10=15.0。这意味着什么?你会采取什么行动?

  3. 为什么 K8s 的 CPU Manager static 策略要求 Requests = Limits?如果不相等会发生什么?


第四章:eBPF 与 Perf 微观性能透视

4.1 学习目标

完成本章学习后,你将能够:

  1. 理解 eBPF 相比 strace 的零侵入优势与安全边界

  2. 使用 BCC/Cilium Hubble 定位 TCP 重传、DNS 延迟、HTTP/gRPC 瓶颈

  3. 生成并解读 On-CPU/Off-CPU 火焰图

  4. 构建 eBPF + cAdvisor + 业务 RED 指标的交叉验证矩阵

4.2 告别 strace:传统排查手段的致命缺陷

4.2.1 strace 的三大问题

4.2.2 eBPF 的革命性优势

eBPF(Extended Berkeley Packet Filter)允许在不修改内核源码、不加载内核模块的前提下,动态向内核注入安全的"探针"代码:

Plain
传统方式:
用户态进程 → ptrace 暂停 → 读取寄存器 → 恢复执行(高开销)

eBPF 方式:
内核事件触发 → eBPF 虚拟机执行探针代码 → 结果写入 Map → 用户态读取(零停顿)

关键优势

  • 零侵入:不暂停目标进程,不影响业务执行

  • 内核可见:可以 hook 任何内核函数、tracepoint、kprobe

  • 安全:eBPF Verifier 确保程序不会崩溃内核

  • 高效:JIT 编译为本地机器码,开销 < 1%

4.3 eBPF 实战:网络问题透视

4.3.1 TCP 重传定位

当应用出现偶发超时,但节点带宽未跑满时,很可能是 TCP 重传:

Bash
# 使用 BCC tcptracer 监控 TCP 事件
/usr/share/bcc/tools/tcptracer

# 只关注重传事件
/usr/share/bcc/tools/tcptracer -R
# 输出示例:
# Tracing TCP events...
# T  PID    COMM         IP SADDR            DADDR            SPORT  DPORT
# R  12345  java         4  10.0.1.5         10.0.2.10        45678  8080
# R  12345  java         4  10.0.1.5         10.0.2.10        45679  8080
# ← 大量重传指向同一目标 10.0.2.10:8080

# 使用 tcpdrop 查看丢包原因
/usr/share/bcc/tools/tcpdrop
# 输出示例:
# TIME     PID    IP SADDR:DPORT          DADDR:SPORT          STATE (TCPFLAGS)
# 10:22:15 12345  4  10.0.1.5:45678      10.0.2.10:8080       ESTABLISHED (ACK)
#     tcp_drop+0x1
#     tcp_rcv_state_process+0x1a2
#     tcp_v4_do_rcv+0x8f
# ← 内核栈显示丢包发生在接收处理阶段

4.3.2 DNS 解析延迟分析

K8s 环境中,CoreDNS 延迟直接影响服务发现:

Bash
# 使用 gethostlatency 监控 DNS 解析耗时
/usr/share/bcc/tools/gethostlatency
# 输出示例:
# TIME      PID    COMM                  LATms HOST
# 10:22:15  12345  java                  0.05  service-a.default.svc.cluster.local
# 10:22:16  12345  java                152.30  service-b.default.svc.cluster.local  ← 异常!
# 10:22:17  12678  python3               0.03  service-a.default.svc.cluster.local

# 如果延迟 > 100ms,进一步排查:
# 1. CoreDNS Pod 状态
kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide

# 2. CoreDNS 延迟指标(Prometheus)
# coredns_dns_request_duration_seconds_bucket

# 3. 节点 conntrack UDP 冲突(常见根因!)
conntrack -S | grep "insert_failed"
# 如果 insert_failed > 0,说明 UDP conntrack 冲突导致 DNS 包丢失

DNS 延迟的常见根因

4.3.3 Cilium Hubble:全链路网络可视化

Bash
# 安装 Hubble CLI
hubble observe --pod java-app --type drop
# 输出示例:
# 🚀 Flow: java-app-7d8f9 -> service-b:8080 DROPPED (Policy denied)

# 查看特定 Pod 的 TCP 重传
hubble observe --pod java-app --protocol tcp --verdict DROPPED

# 查看 DNS 查询延迟
hubble observe --type l7 --protocol dns --pod java-app

4.4 Perf 实战:火焰图定位热点函数

4.4.1 安全采样原则

4.4.2 On-CPU 火焰图(CPU 消耗在哪里)

Bash
# 步骤1:找到目标容器的主进程 PID
CONTAINER_ID=$(crictl ps --name java-app -q)
PID=$(crictl inspect $CONTAINER_ID | jq '.info.pid')

# 步骤2:安全采样(99Hz,30秒)
perf record -F 99 -p $PID -g -- sleep 30

# 步骤3:生成火焰图
perf script | \
  /opt/FlameGraph/stackcollapse-perf.pl | \
  /opt/FlameGraph/flamegraph.pl \
    --title "On-CPU Flame Graph - java-app" \
    --colors java \
    > oncpu-flamegraph.svg

# 步骤4:解读火焰图
# - 宽度 = CPU 时间占比(越宽越耗时)
# - 高度 = 调用栈深度
# - 平顶山 = 热点函数(优化目标)
# - 红色 = 用户态代码
# - 黄色/橙色 = 内核态代码

4.4.3 Off-CPU 火焰图(CPU 等待在哪里)

Off-CPU 分析比 On-CPU 更重要——它揭示进程为什么慢(在等什么):

Bash
# 使用 BCC offcputime 工具
/usr/share/bcc/tools/offcputime -df -p $PID 30 > offcpu.stacks

# 生成 Off-CPU 火焰图
/opt/FlameGraph/flamegraph.pl \
  --title "Off-CPU Flame Graph" \
  --colors io \
  --countname "us" \
  offcpu.stacks > offcpu-flamegraph.svg

# 解读:
# - 如果大量时间在 io_schedule → 等待磁盘 I/O
# - 如果大量时间在 futex_wait → 用户态锁竞争
# - 如果大量时间在 mutex_lock → 内核态锁竞争
# - 如果大量时间在 tcp_recvmsg → 等待网络数据
# - 如果大量时间在 schedule → 被 CFS Throttling 挂起

4.4.4 使用 eBPF 替代 Perf(更安全)

Bash
# 使用 BCC profile 工具(基于 eBPF,比 perf 更安全)
/usr/share/bcc/tools/profile -df -p $PID 30 > profile.stacks

# 使用 cpudist 查看调度延迟分布
/usr/share/bcc/tools/cpudist -p $PID 10
# 输出示例:
#     usecs               : count     distribution
#         0 -> 1          : 0        |                    |
#         2 -> 3          : 125      |********************|
#         4 -> 7          : 89       |**************      |
#         8 -> 15         : 45       |*******             |
#        16 -> 31         : 12       |*                   |
#        32 -> 63         : 3        |                    |
#      1024 -> 2047       : 8        |*                   |  ← 异常!被 Throttling
#      2048 -> 4095       : 5        |                    |

# 使用 runqlat 查看运行队列延迟
/usr/share/bcc/tools/runqlat -p $PID 10

4.5 交叉验证矩阵

eBPF/Perf 提供微观视角,必须与宏观指标交叉验证:

4.6 实战 Lab

Lab 4.1:CoreDNS 延迟注入与 eBPF 定位

Bash
# 步骤1:在 CoreDNS Pod 上注入延迟(使用 tc netem)
COREDNS_POD=$(kubectl get pods -n kube-system -l k8s-app=kube-dns -o name | head -1)
kubectl exec -n kube-system $COREDNS_POD -- tc qdisc add dev eth0 root netem delay 100ms

# 步骤2:在业务节点使用 gethostlatency 观察
/usr/share/bcc/tools/gethostlatency
# 应看到所有 DNS 查询延迟 > 100ms

# 步骤3:使用 tcptracer 观察 UDP 重传
/usr/share/bcc/tools/tcptracer  # 注意:UDP 需要用 udptracer 或 Hubble

# 步骤4:检查 conntrack UDP 冲突
conntrack -S | grep insert_failed

# 步骤5:清理
kubectl exec -n kube-system $COREDNS_POD -- tc qdisc del dev eth0 root

Lab 4.2:Off-CPU 火焰图定位锁竞争

Bash
# 步骤1:部署一个有锁竞争的应用
# (使用一个多线程 Java 应用,大量 synchronized 块)

# 步骤2:生成 Off-CPU 火焰图
PID=$(crictl inspect $(crictl ps --name java-app -q) | jq '.info.pid')
/usr/share/bcc/tools/offcputime -df -p $PID 30 > offcpu.stacks
/opt/FlameGraph/flamegraph.pl --colors io offcpu.stacks > offcpu.svg

# 步骤3:分析火焰图
# 如果看到大量时间在:
# futex_wait_queue_me → futex_wait → do_futex → __x64_sys_futex
# 说明用户态锁竞争严重

# 步骤4:使用 perf 验证
perf record -F 99 -p $PID -g -- sleep 10
perf report
# 确认热点函数

4.7 工具链速查表

4.8 安全红线

  • 🚫 生产节点禁止使用 perf record -F 999,采样频率 ≤ 99Hz

  • 🚫 eBPF 程序必须经过 Verifier,禁止在 < 4.15 内核加载未签名探针

  • 🚫 禁止在 CPU > 90% 或 Load > 2×核心数 的节点上启动新的 eBPF 程序

  • ✅ 优先使用 DaemonSet 部署只读 eBPF Agent,避免 SSH 登录假死节点

  • ✅ 所有 eBPF 工具必须设置超时(--duration),防止无限运行

4.9 最佳实践 Checklist

  • [ ] 每个 K8s 节点部署 BCC 工具集(DaemonSet 或按需安装)

  • [ ] 大规模集群部署 Cilium + Hubble 替代 iptables + tcpdump

  • [ ] 部署 Parca/Pyroscope 持续剖析平台,保留 7 天历史火焰图

  • [ ] 建立 eBPF → cAdvisor → RED 三层交叉验证 SOP

  • [ ] 所有 eBPF 排查操作记录在 RCA 报告中,包含工具版本和参数

4.10 思考题

  1. 为什么 strace -p <PID> 在高并发 Java 应用上可能导致服务完全不可用?eBPF 如何避免这个问题?

  2. Off-CPU 火焰图显示大量时间在 schedule() 函数。这一定意味着 CPU 不足吗?还有哪些可能?

  3. 在不重启 Pod、不修改代码的前提下,如何定位一个 Go 应用的 goroutine 锁竞争问题?


第五章:节点假死与崩溃现场取证

5.1 学习目标

完成本章学习后,你将能够:

  1. 配置 Serial Console 与 kdump,确保假死/panic 时仍可获取现场

  2. 使用 NodeProblemDetector + 自定义 kmsg 规则捕获内核级异常

  3. 构建"黑匣子"自动快照机制,解决"故障后无证据"难题

  4. 掌握假死节点的五步取证法

5.2 为什么需要"黑匣子"机制

当节点彻底假死(SSH 连不上、Kubelet 无响应、监控断连)时,传统排查手段全部失效:

核心问题:故障发生后,现场证据消失。我们需要一套独立于用户态的"黑匣子"机制。

5.3 Serial Console(串行控制台)

5.3.1 原理

Serial Console 通过硬件串口(UART)直接连接 CPU,绕过所有软件层(包括内核调度器)。即使系统完全假死,只要 CPU 还在执行指令,Serial Console 就能输出内核日志。

5.3.2 配置方法

云平台配置(以阿里云/AWS 为例):

Bash
# 阿里云 ECS:在控制台启用"串行控制台"
# AWS EC2:启用 EC2 Serial Console

# 配置 GRUB 输出到串口
# /etc/default/grub
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8"
GRUB_TERMINAL="serial console"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"

# 更新 GRUB
grub2-mkconfig -o /boot/grub2/grub.cfg

本地访问

Bash
# 物理机:通过 IPMI/BMC 的 SOL(Serial Over LAN)
ipmitool -H <BMC_IP> -U admin -P password sol activate

# 云平台:通过 Web 控制台或 CLI
# 阿里云
aliyun ecs DescribeInstanceConsoleOutput --InstanceId i-xxx

# AWS
aws ec2 get-console-output --instance-id i-xxx

5.3.3 假死时的关键信息

通过 Serial Console,你可以看到:

Plain
[  123.456789] NMI watchdog: BUG: soft lockup - CPU#3 stuck for 23s! [kworker/3:1:12345]
[  123.456790] Modules linked in: nf_conntrack iptable_nat ...
[  123.456791] CPU: 3 PID: 12345 Comm: kworker/3:1 Not tainted 5.15.0-91-generic
[  123.456792] RIP: 0010:native_queued_spin_lock_slowpath+0x1c2/0x2e0
[  123.456793] Call Trace:
[  123.456794]  _raw_spin_lock+0x28/0x30
[  123.456795]  nf_conntrack_confirm+0x25/0x50
[  123.456796]  nf_hook_slow+0x43/0xc0

关键信息提取

  • soft lockup:CPU 软锁死,内核态死循环

  • RIP:出错时的指令地址(定位到具体函数)

  • Call Trace:内核调用栈(定位到具体代码路径)

  • Modules linked in:加载的内核模块(排查驱动问题)

5.4 kdump:内核崩溃转储

5.4.1 原理

kdump 在内核 panic 时,启动一个预留的迷你内核(capture kernel),将崩溃时的完整内存镜像(vmcore)写入磁盘。事后可以使用 crash 工具分析 vmcore。

Plain
正常运行内核(Production Kernel)
        ↓ panic 触发
    kexec 跳转

预留的捕获内核(Capture Kernel)

    将崩溃时的内存 dump 到 /var/crash/vmcore

    重启回正常内核

5.4.2 配置方法

Bash
# 步骤1:预留崩溃内核内存
# /etc/default/grub
GRUB_CMDLINE_LINUX="crashkernel=512M"
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

# 步骤2:安装 kdump 工具
# RHEL/CentOS
yum install kexec-tools crash
systemctl enable kdump
systemctl start kdump

# Ubuntu/Debian
apt install kdump-tools crash
systemctl enable kdump-tools

# 步骤3:验证 kdump 就绪
kdumpctl status
# 输出:Kdump is operational

# 步骤4:配置 vmcore 存储路径
# /etc/kdump.conf
path /var/crash
core_collector makedumpfile -l --message-level 7 -d 31
# -d 31:压缩并过滤无用页面,减小 vmcore 体积

# 步骤5:测试 kdump(谨慎!会导致节点重启)
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger
# 节点会 panic → kdump → 重启
# 重启后检查 /var/crash/ 下是否有 vmcore

5.4.3 分析 vmcore

Bash
# 使用 crash 工具打开 vmcore
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/127.0.0.1-2025-06-09-10:22:15/vmcore

# 在 crash 交互界面中:
crash> bt                    # 查看 panic 时的调用栈
crash> bt -a                 # 查看所有 CPU 的调用栈
crash> ps                    # 查看进程列表
crash> ps -m                 # 查看 D-State 进程
crash> log                   # 查看内核日志(dmesg)
crash> dev -d                # 查看磁盘设备
crash> net                   # 查看网络设备
crash> kmem -i               # 查看内存使用
crash> mod                   # 查看加载的内核模块
crash> dis -l <address>      # 反汇编特定地址

5.5 NodeProblemDetector(NPD)深度配置

5.5.1 工作机制

NPD 是 K8s 生态中用于监控节点健康状况的守护进程,将底层系统问题转化为 K8s API Server 能理解的 NodeCondition 或 Event:

Plain
系统日志/脚本 → NPD 检测 → NodeCondition/Event → K8s Scheduler → 调度决策

                                              Taint 效应
                                              (阻止新 Pod 调度)

5.5.2 生产级配置

JSON
{
  "plugin": "kmsg",
  "pluginConfig": {
    "source": "kmsg",
    "logPath": "/dev/kmsg",
    "lookback": "5m",
    "bufferSize": 1024,
    "source": "kmsg",
    "metricsReporting": true
  },
  "logPath": "/dev/kmsg",
  "lookback": "5m",
  "bufferSize": 1024,
  "source": "kmsg",
  "conditions": [
    {
      "type": "KernelSoftLockup",
      "reason": "NoSoftLockup",
      "message": "Node has no soft lockup"
    },
    {
      "type": "ReadonlyFilesystem",
      "reason": "FilesystemIsNotReadOnly",
      "message": "Filesystem is not read-only"
    },
    {
      "type": "OOMKilling",
      "reason": "NoOOMKilling",
      "message": "No OOM killing detected"
    }
  ],
  "rules": [
    {
      "type": "permanent",
      "condition": "KernelSoftLockup",
      "reason": "KernelHasSoftLockup",
      "pattern": "NMI watchdog: BUG: Soft Lockup",
      "message": "CPU soft lockup detected"
    },
    {
      "type": "permanent",
      "condition": "ReadonlyFilesystem",
      "reason": "FilesystemIsReadOnly",
      "pattern": "Remounting filesystem read-only",
      "message": "Filesystem has been remounted read-only"
    },
    {
      "type": "temporary",
      "reason": "OOMKilling",
      "pattern": "Killed process \\d+ (.+) total-vm.*",
      "message": "OOM killing detected"
    },
    {
      "type": "permanent",
      "condition": "KernelDeadlock",
      "reason": "KernelHasDeadlock",
      "pattern": "task \\S+ blocked for more than \\d+ seconds",
      "message": "Kernel task blocked (possible deadlock)"
    },
    {
      "type": "permanent",
      "condition": "RCUStall",
      "reason": "KernelHasRCUStall",
      "pattern": "rcu_sched self-detected stall on CPU",
      "message": "RCU stall detected"
    }
  ]
}

5.5.3 部署为 Static Pod(关键!)

NPD 必须以 Static Pod 或 systemd service 运行,不能依赖 Kubelet 健康:

YAML
# /etc/kubernetes/manifests/node-problem-detector.yaml(Static Pod)
apiVersion: v1
kind: Pod
metadata:
  name: node-problem-detector
  namespace: kube-system
  labels:
    app: node-problem-detector
spec:
  priorityClassName: system-node-critical
  hostNetwork: true
  hostPID: true
  containers:
  - name: node-problem-detector
    image: registry.k8s.io/node-problem-detector/node-problem-detector:v0.8.18
    command:
    - /node-problem-detector
    - --logtostderr
    - --config.system-log-monitor=/config/kernel-monitor.json
    - --config.custom-plugin-monitor=/config/custom-monitor.json
    securityContext:
      privileged: true
    resources:
      requests:
        cpu: 50m
        memory: 64Mi
      limits:
        cpu: 200m
        memory: 128Mi
    volumeMounts:
    - name: kmsg
      mountPath: /dev/kmsg
      readOnly: true
    - name: config
      mountPath: /config
      readOnly: true
    - name: localtime
      mountPath: /etc/localtime
      readOnly: true
  volumes:
  - name: kmsg
    hostPath:
      path: /dev/kmsg
  - name: config
    hostPath:
      path: /etc/npd/config
  - name: localtime
    hostPath:
      path: /etc/localtime

5.6 Sysstat 黑匣子:长期采样

5.6.1 配置

Bash
# 安装 sysstat
yum install sysstat  # RHEL
apt install sysstat  # Debian

# 配置 10 秒采样间隔
# /etc/sysconfig/sysstat(RHEL)或 /etc/default/sysstat(Debian)
HISTORY=31           # 保留 31 天
COMPRESSAFTER=7      # 7 天后压缩

# /etc/cron.d/sysstat
# 修改采样间隔为 10 秒
*/1 * * * * root /usr/lib64/sa/sa1 6 10
# 含义:每分钟执行一次,每次采样 6 个数据点,间隔 10 秒

# 启用服务
systemctl enable sysstat
systemctl start sysstat

5.6.2 故障后回溯

Bash
# 查看特定日期的 CPU 数据
sar -P ALL -f /var/log/sa/sa09
# sa09 = 6月9日的数据

# 查看负载
sar -q -f /var/log/sa/sa09

# 查看网络
sar -n DEV -f /var/log/sa/sa09

# 查看磁盘 I/O
sar -d -f /var/log/sa/sa09

# 查看内存
sar -r -f /var/log/sa/sa09

# 查看特定时间段(10:20-10:30)
sar -P ALL -f /var/log/sa/sa09 -s 10:20:00 -e 10:30:00

5.7 SOP:假死节点取证五步法

Plain
┌─────────────────────────────────────────────────────────┐
│              假死节点取证五步法                           │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  步骤1:尝试带外接入                                     │
│  ├─ Serial Console / IPMI SOL / 云平台控制台             │
│  └─ 如果可接入 → 步骤2                                  │
│  └─ 如果不可接入 → 步骤3                                │
│                                                         │
│  步骤2:在线取证(节点仍可响应串口)                      │
│  ├─ dmesg -T > /tmp/kmsg.txt                           │
│  ├─ sar -A > /tmp/sar.txt                              │
│  ├─ ps auxf > /tmp/ps.txt                              │
│  ├─ cat /proc/loadavg > /tmp/load.txt                  │
│  └─ cat /proc/softirqs > /tmp/softirqs.txt             │
│                                                         │
│  步骤3:离线取证(节点完全无响应)                        │
│  ├─ 检查 /var/crash/ 下是否有 vmcore(kdump)           │
│  ├─ 检查云平台控制台截图/日志                            │
│  └─ 检查 NPD 上报的最后一个 Event                       │
│                                                         │
│  步骤4:关联分析                                         │
│  ├─ 提取 NPD Event 时间戳                              │
│  ├─ 提取 Prometheus 断点前最后数据                       │
│  ├─ 提取 sysstat 对应时间段数据                          │
│  └─ 关联告警 ID、变更窗口、部署记录                      │
│                                                         │
│  步骤5:归档与 RCA                                      │
│  ├─ 所有证据上传至 S3/Loki                              │
│  ├─ 关联告警 ID 与时间窗                                │
│  └─ 撰写 RCA 报告(见第十章模板)                       │
│                                                         │
└─────────────────────────────────────────────────────────┘

5.8 实战 Lab

Lab 5.1:触发 Soft Lockup 并通过 Serial Console 捕获

Bash
# 步骤1:确认 Serial Console 已配置
dmesg | grep "console.*ttyS0"

# 步骤2:使用内核模块触发 soft lockup(仅测试环境!)
# 加载一个会死循环的内核模块
cat > /tmp/lockup_test.c << 'EOF'
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/delay.h>

static int __init lockup_init(void) {
    printk(KERN_ERR "Triggering soft lockup for testing...\n");
    while(1) { mdelay(1000); }  // 死循环
    return 0;
}

static void __exit lockup_exit(void) {}

module_init(lockup_init);
module_exit(lockup_exit);
MODULE_LICENSE("GPL");
EOF

# 编译并加载(会导致一个 CPU 核心锁死!)
make -C /lib/modules/$(uname -r)/build M=/tmp modules
insmod /tmp/lockup_test.ko

# 步骤3:等待 NMI watchdog 触发(默认 20 秒)
# 通过 Serial Console 观察输出:
# "NMI watchdog: BUG: soft lockup - CPU#X stuck for XXs!"

# 步骤4:验证 NPD 是否捕获
kubectl get events --field-selector reason=KernelHasSoftLockup

Lab 5.2:kdump 配置与 vmcore 分析

Bash
# 步骤1:配置 kdump(见 5.4.2)
kdumpctl status  # 确认 operational

# 步骤2:触发 panic(仅测试环境!)
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger

# 步骤3:节点重启后,检查 vmcore
ls -lh /var/crash/
# 输出:
# drwxr-xr-x. 2 root root 4096 Jun  9 10:25 127.0.0.1-2025-06-09-10:22:15

# 步骤4:分析 vmcore
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux \
  /var/crash/127.0.0.1-2025-06-09-10:22:15/vmcore

crash> bt
# 查看 panic 调用栈

crash> log | tail -50
# 查看 panic 前的内核日志

5.9 最佳实践 Checklist

  • [ ] 所有生产节点必须配置 Serial Console(云平台)或 IPMI SOL(物理机)

  • [ ] 所有生产节点必须启用 kdump,并验证 vmcore 生成路径有足够空间

  • [ ] NPD 必须以 Static Pod 部署,不依赖 Kubelet 健康

  • [ ] sysstat 采样间隔 ≤ 10 秒,数据保留 ≥ 31 天

  • [ ] 假死节点取证必须在重启前完成(或确认 kdump 已生成 vmcore)

  • [ ] 所有取证数据自动归档至对象存储,关联告警 ID

5.10 思考题

  1. 为什么 NPD 不能以普通 DaemonSet 部署?在什么场景下 DaemonSet 会失效?

  2. kdump 预留的 crashkernel=512M 内存对节点有什么影响?在大内存节点上应该如何调整?

  3. 如果节点假死且没有配置 Serial Console 和 kdump,你还有哪些方法获取故障信息?


第六章:自动化诊断与自愈体系

6.1 学习目标

完成本章学习后,你将能够:

  1. 设计 NPD + 自定义脚本 + K8s 驱逐的三层自愈闭环

  2. 配置 Kubelet 驱逐阈值与 QoS 优先级保障

  3. 构建 Ansible/Argo Workflows 驱动的故障响应流水线

  4. 实现从"人工救火"到"自动免疫"的运维模式转变

6.2 三层自愈架构

Plain
┌─────────────────────────────────────────────────────────────┐
│                    三层自愈架构                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第一层:感知(Detection)                                   │
│  ├─ NPD:内核级异常(soft lockup / OOM / readonly fs)       │
│  ├─ 自定义脚本:PLEG 超时 / containerd D-State / 网络连通性  │
│  ├─ Prometheus 告警:资源阈值 / 业务 RED 指标                │
│  └─ Kubelet 内置:节点条件(MemoryPressure / DiskPressure)  │
│                                                             │
│  第二层:隔离(Isolation)                                   │
│  ├─ 自动打污点(Taint):阻止新 Pod 调度                     │
│  ├─ 网关限流/降级:减少故障节点流量                           │
│  └─ 服务网格熔断:Istio/Linkerd 自动熔断                     │
│                                                             │
│  第三层:恢复(Recovery)                                    │
│  ├─ Kubelet 驱逐:按 QoS 优先级逐步驱逐 Pod                  │
│  ├─ kubectl drain:优雅迁移现有 Pod                          │
│  ├─ ASG/CA 替换:终止故障实例,创建新节点                     │
│  └─ 物理机带外重启:IPMI/BMC 远程重启                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.3 自定义诊断脚本实战

6.3.1 PLEG 健康检查

PLEG(Pod Lifecycle Event Generator)负责维护容器运行时状态与 K8s API 的一致性。当 containerd 响应缓慢时,PLEG 超时,节点变为 NotReady。

Bash
#!/bin/bash
# /opt/npd/plugins/check_pleg.sh
# NPD 自定义插件:检测 PLEG 健康状态

KUBELET_LOG="/var/log/kubelet.log"
THRESHOLD=5
TIME_WINDOW="5m"

# 获取最近5分钟内 PLEG 报错次数
ERROR_COUNT=$(journalctl -u kubelet --since "${TIME_WINDOW} ago" 2>/dev/null | \
  grep -c "PLEG is not healthy" || echo 0)

if [ "$ERROR_COUNT" -ge "$THRESHOLD" ]; then
    echo "PLEG unhealthy: ${ERROR_COUNT} errors in last ${TIME_WINDOW}"
    exit 1  # 非零退出码触发 NPD 告警
fi

echo "PLEG healthy"
exit 0

6.3.2 综合节点健康检查

Bash
#!/bin/bash
# /opt/npd/plugins/node_health_check.sh
# 综合节点健康检查脚本

ISSUES=()

# 检查1:containerd 是否处于 D 状态
CONTAINERD_STATE=$(ps -eo stat,comm | grep containerd | awk '{print $1}')
if ; then
    ISSUES+=("containerd in D-state")
fi

# 检查2:磁盘 I/O 饱和度
DISK_UTIL=$(iostat -xz 1 2 | tail -n +4 | awk '{if($NF+0 > 95) print $1}')
if [ -n "$DISK_UTIL" ]; then
    ISSUES+=("Disk saturated: $DISK_UTIL")
fi

# 检查3:APIServer 连通性
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
if ! curl -sk --connect-timeout 5 "${APISERVER}/healthz" | grep -q "ok"; then
    ISSUES+=("APIServer unreachable")
fi

# 检查4:TIME_WAIT 连接数
TW_COUNT=$(ss -tan state time-wait | wc -l)
if [ "$TW_COUNT" -gt 20000 ]; then
    ISSUES+=("TIME_WAIT exhaustion: $TW_COUNT")
fi

# 检查5:D-State 进程数
D_COUNT=$(ps -eo state | grep -c D)
if [ "$D_COUNT" -gt 10 ]; then
    ISSUES+=("D-state processes: $D_COUNT")
fi

# 输出结果
if [ ${#ISSUES[@]} -gt 0 ]; then
    echo "Node unhealthy: ${ISSUES[*]}"
    exit 1
fi

echo "Node healthy"
exit 0

6.3.3 NPD 自定义插件配置

JSON
{
  "plugin": "custom",
  "pluginConfig": {
    "invoke_interval": "30s",
    "timeout": "10s",
    "max_output_length": 256,
    "concurrency": 3
  },
  "source": "node-health-check",
  "conditions": [
    {
      "type": "NodeHealthy",
      "reason": "NodeIsHealthy",
      "message": "Node is healthy"
    }
  ],
  "rules": [
    {
      "type": "permanent",
      "condition": "NodeHealthy",
      "reason": "NodeIsUnhealthy",
      "path": "/opt/npd/plugins/node_health_check.sh",
      "message": "Node health check failed"
    }
  ]
}

6.4 Kubelet 驱逐策略精细配置

6.4.1 驱逐阈值

YAML
# KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration

# 硬驱逐:立即触发,不等待优雅退出
evictionHard:
  memory.available: "100Mi"
  nodefs.available: "5%"
  nodefs.inodesFree: "5%"
  imagefs.available: "10%"
  pid.available: "100"

# 软驱逐:持续一段时间后才触发
evictionSoft:
  memory.available: "250Mi"
  nodefs.available: "10%"
  imagefs.available: "15%"

evictionSoftGracePeriod:
  memory.available: "30s"
  nodefs.available: "30s"
  imagefs.available: "30s"

# 驱逐后的最小回收量
evictionMinimumReclaim:
  memory.available: "500Mi"
  nodefs.available: "1Gi"
  imagefs.available: "2Gi"

# 驱逐压力下的 Pod 终止宽限期
evictionMaxPodGracePeriod: 30

# 资源预留
kubeReserved:
  cpu: "500m"
  memory: "1Gi"
  ephemeral-storage: "10Gi"

systemReserved:
  cpu: "250m"
  memory: "512Mi"
  ephemeral-storage: "5Gi"

6.4.2 QoS 驱逐顺序

K8s 在资源压力下按以下顺序驱逐 Pod:

Plain
驱逐顺序(从先到后):
1. BestEffort(无 Requests/Limits)
2. Burstable(Requests < Limits)
3. Guaranteed(Requests = Limits)← 最后被驱逐

同 QoS 等级内,按以下优先级:
- 实际使用量超过 Requests 最多的优先驱逐
- Priority 值低的优先驱逐

6.4.3 PodDisruptionBudget 保护

YAML
# 确保核心服务在驱逐时不会全部下线
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: core-api-pdb
spec:
  minAvailable: "80%"    # 至少 80% 的 Pod 保持可用
  selector:
    matchLabels:
      app: core-api
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: database-pdb
spec:
  maxUnavailable: 1      # 最多 1 个 Pod 不可用
  selector:
    matchLabels:
      app: postgresql

6.5 自动化运维流水线

6.5.1 Alertmanager Webhook → Ansible 自愈

YAML
# Alertmanager 配置
route:
  receiver: 'auto-remediation'
  routes:
  - match:
      alertname: NodeCPULoadHigh
      severity: critical
    receiver: 'auto-remediation'
    continue: true

receivers:
- name: 'auto-remediation'
  webhook_configs:
  - url: 'http://ansible-tower:8080/api/v2/job_templates/42/launch/'
    http_config:
      bearer_token: '<token>'
    send_resolved: true
YAML
# Ansible Playbook: auto-remediate-node.yml
---
- name: Auto Remediate Unhealthy Node
  hosts: localhost
  vars:
    node_name: "{{ alertmanager_payload.commonLabels.instance }}"
  tasks:
    - name: Cordon the node
      command: kubectl cordon {{ node_name }}

    - name: Wait for node to be unschedulable
      command: kubectl get node {{ node_name }} -o jsonpath='{.spec.unschedulable}'
      register: cordon_status
      until: cordon_status.stdout == "true"
      retries: 5
      delay: 5

    - name: Drain the node
      command: >
        kubectl drain {{ node_name }}
        --ignore-daemonsets
        --delete-emptydir-data
        --timeout=300s
        --force
      register: drain_result
      failed_when: false

    - name: Collect diagnostics before reboot
      command: >
        ssh {{ node_name }}
        "dmesg -T > /tmp/diag_dmesg.txt;
         sar -A > /tmp/diag_sar.txt;
         ps auxf > /tmp/diag_ps.txt;
         tar czf /tmp/diag_$(date +%s).tar.gz /tmp/diag_*.txt"
      failed_when: false

    - name: Upload diagnostics to S3
      command: >
        aws s3 cp /tmp/diag_*.tar.gz
        s3://ops-diagnostics/{{ node_name }}/$(date +%Y%m%d)/
      failed_when: false

    - name: Trigger ASG instance replacement (cloud)
      command: >
        aws autoscaling terminate-instance-in-auto-scaling-group
        --instance-id {{ instance_id }}
        --should-decrement-desired-capacity
      when: cloud_provider == "aws"

6.5.2 自愈闭环验证

Bash
# 验证自愈流水线完整性
# 1. 模拟节点 CPU 过载
stress-ng --cpu $(nproc) --timeout 300s &

# 2. 等待告警触发(Prometheus → Alertmanager → Webhook)
# 观察 Alertmanager UI:http://alertmanager:9093

# 3. 验证节点被 cordon
kubectl get node <node-name> -o jsonpath='{.spec.unschedulable}'
# 应输出:true

# 4. 验证 Pod 被驱逐
kubectl get pods --field-selector spec.nodeName=<node-name>
# 应只剩 DaemonSet Pod

# 5. 验证诊断数据已上传
aws s3 ls s3://ops-diagnostics/<node-name>/

# 6. 验证新节点加入(ASG 场景)
kubectl get nodes -w

6.6 最佳实践 Checklist

  • [ ] NPD 必须以 Static Pod 或 systemd service 运行,不依赖 Kubelet 健康

  • [ ] 诊断脚本执行超时 ≤ 10s,禁止调用 docker ps 等可能阻塞的命令

  • [ ] 自动驱逐前必须检查 PodDisruptionBudget,防止核心服务全部下线

  • [ ] 自愈动作必须记录审计日志:触发条件、执行命令、结果状态、操作人(系统)

  • [ ] 每月进行一次自愈流水线演练(Game Day),验证端到端有效性

  • [ ] 驱逐超时设置合理值(300s),避免 PDB 阻塞导致 drain 永远无法完成

6.7 思考题

  1. 如果 NPD 检测到节点异常并打了 Taint,但 Kubelet 本身已经假死无法执行驱逐,你会如何设计兜底方案?

  2. 在混合云环境(部分物理机 + 部分云 VM)中,如何统一自愈流水线?物理机的"替换"操作应该如何实现?

  3. 自动驱逐可能触发"驱逐风暴"(大量 Pod 同时迁移导致其他节点过载)。你会如何设计防护机制?


第七章:可观测性盲区治理与监控重构

7.1 学习目标

完成本章学习后,你将能够:

  1. 分析监控断连的三大根因(采集超时/网络拥塞/OOM 误伤)

  2. 构建双路采集 + Remote Write + Static Pod 兜底的高可用架构

  3. 实施告警分级、收敛、抑制与自动化现场快照

  4. 设计故障回溯能力,消灭"无法复现"的借口

7.2 监控数据断连的根因分析

7.2.1 三大根因

7.2.2 断连时间线分析

Plain
正常状态:
Prometheus ──scrape──→ Node Exporter ──→ 指标数据 ──→ TSDB
   每15s                    200 OK

故障开始(CPU 100%):
Prometheus ──scrape──→ Node Exporter ──→ 超时(无 CPU 时间片)
   每15s                    timeout

故障加剧(网络拥塞):
Prometheus ──scrape──→ [网络丢包] ──→ 完全断连
   每15s                    DROP

故障恢复后:
Prometheus ──scrape──→ Node Exporter ──→ 200 OK(但中间数据永久丢失)

7.3 高可用采集架构

7.3.1 架构设计

Plain
┌─────────────────────────────────────────────────────────────┐
│                    高可用监控采集架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  节点层:                                                    │
│  ├─ Node Exporter(Static Pod, system-node-critical)        │
│  ├─ Prometheus Agent(DaemonSet, remote_write 模式)         │
│  └─ eBPF Agent(DaemonSet, 持续剖析)                        │
│                                                             │
│  传输层:                                                    │
│  ├─ remote_write → VictoriaMetrics(主)                     │
│  └─ remote_write → Thanos Receive(备)                      │
│                                                             │
│  存储层:                                                    │
│  ├─ VictoriaMetrics(热数据 7天)                            │
│  ├─ Thanos Store(温数据 30天,S3 后端)                     │
│  └─ S3/GCS(冷数据 1年)                                    │
│                                                             │
│  展示层:                                                    │
│  ├─ Grafana(统一查询)                                      │
│  └─ Alertmanager(告警路由)                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.3.2 Node Exporter Static Pod 配置

YAML
# /etc/kubernetes/manifests/node-exporter.yaml
apiVersion: v1
kind: Pod
metadata:
  name: node-exporter
  namespace: monitoring
  labels:
    app: node-exporter
spec:
  priorityClassName: system-node-critical  # 最高优先级,最后被驱逐
  hostNetwork: true
  hostPID: true
  containers:
  - name: node-exporter
    image: prom/node-exporter:v1.7.0
    args:
    - --path.procfs=/host/proc
    - --path.sysfs=/host/sys
    - --path.rootfs=/host/root
    - --collector.processes
    - --collector.systemd
    - --web.listen-address=0.0.0.0:9100
    ports:
    - containerPort: 9100
      hostPort: 9100
    resources:
      requests:
        cpu: 50m
        memory: 64Mi
      limits:
        cpu: 200m
        memory: 128Mi
    securityContext:
      runAsUser: 0
      readOnlyRootFilesystem: true
    volumeMounts:
    - name: proc
      mountPath: /host/proc
      readOnly: true
    - name: sys
      mountPath: /host/sys
      readOnly: true
    - name: root
      mountPath: /host/root
      readOnly: true
      mountPropagation: HostToContainer
  volumes:
  - name: proc
    hostPath:
      path: /proc
  - name: sys
    hostPath:
      path: /sys
  - name: root
    hostPath:
      path: /
  tolerations:
  - operator: Exists  # 容忍所有污点,确保任何节点都能运行

7.3.3 Prometheus Agent 模式

YAML
# prometheus-agent.yaml
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: agent
  namespace: monitoring
spec:
  mode: Agent  # Agent 模式:无本地 TSDB,仅 remote_write
  replicas: 1
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi
  remoteWrite:
  - url: "http://victoriametrics:8428/api/v1/write"
    queueConfig:
      capacity: 10000
      maxShards: 50
      minShards: 5
      maxSamplesPerSend: 2000
      batchSendDeadline: "5s"
  - url: "http://thanos-receive:19291/api/v1/receive"
    queueConfig:
      capacity: 5000
      maxShards: 20
  scrapeInterval: 15s
  priorityClassName: system-node-critical

7.4 告警治理

7.4.1 告警分级体系

7.4.2 Alertmanager 路由与抑制

YAML
# alertmanager.yml
route:
  receiver: 'default'
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
  # P0:电话轰炸
  - match:
      severity: critical
    receiver: 'pagerduty-critical'
    group_wait: 10s
    repeat_interval: 30m
    continue: true

  # P1:IM 强提醒
  - match:
      severity: warning
    receiver: 'slack-warning'
    group_wait: 30s

  # P2:工单
  - match:
      severity: info
    receiver: 'ticket-system'

# 抑制规则:节点 NotReady 时,抑制该节点所有 Pod 告警
inhibit_rules:
- source_match:
    alertname: NodeNotReady
  target_match_re:
    alertname: 'PodCrashLooping|PodNotReady|ContainerOOMKilled'
  equal: ['node']

- source_match:
    alertname: NodeCPULoadHigh
  target_match_re:
    alertname: 'PodLatencyHigh|PodErrorRateHigh'
  equal: ['node']

# 接收器配置
receivers:
- name: 'pagerduty-critical'
  pagerduty_configs:
  - service_key: '<pagerduty-key>'
    severity: critical

- name: 'slack-warning'
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/xxx'
    channel: '#ops-alerts'
    title: '{{ .CommonAnnotations.summary }}'
    text: '{{ .CommonAnnotations.description }}\nRunbook: {{ .CommonAnnotations.runbook_url }}'

- name: 'auto-remediation'
  webhook_configs:
  - url: 'http://ansible-tower:8080/webhook/'

7.4.3 告警规则最佳实践

YAML
# 告警规则必须包含 runbook_url
groups:
- name: node-alerts
  rules:
  - alert: NodeCPULoadHigh
    expr: |
      (
        node_load5{job="node-exporter"}
        / count without(cpu, mode) (node_cpu_seconds_total{mode="idle"})
      ) > 2
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Node {{ $labels.instance }} load is high"
      description: "5m load average is {{ $value | humanize }}x CPU count"
      runbook_url: "https://wiki.internal/runbooks/node-high-load"
      dashboard_url: "https://grafana.internal/d/node-overview?var-instance={{ $labels.instance }}"

  - alert: NodeCPUSoftIRQStorm
    expr: |
      rate(node_softirqs_total{softirq="NET_RX"}[5m]) > 100000
      and
      rate(node_cpu_seconds_total{mode="softirq"}[5m]) > 0.8
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "SoftIRQ storm on {{ $labels.instance }}"
      runbook_url: "https://wiki.internal/runbooks/softirq-storm"

7.5 故障现场自动快照

7.5.1 Webhook 触发诊断脚本

Bash
#!/bin/bash
# /opt/scripts/k8s-diagnose-snapshot.sh
# 由 Alertmanager Webhook 触发的自动诊断脚本

NODE_NAME=$1
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="/tmp/diag_${NODE_NAME}_${TIMESTAMP}"
S3_BUCKET="s3://ops-diagnostics"

mkdir -p $OUTPUT_DIR

# 系统概览
top -bn1 > $OUTPUT_DIR/top.txt
ps -eo pid,ppid,user,stat,wchan:32,%mem,%cpu,cmd --sort=-%cpu | head -50 > $OUTPUT_DIR/ps.txt
vmstat 1 5 > $OUTPUT_DIR/vmstat.txt
mpstat -P ALL 1 3 > $OUTPUT_DIR/mpstat.txt

# 内核日志
dmesg -T > $OUTPUT_DIR/dmesg.txt
journalctl -k --since "10 minutes ago" > $OUTPUT_DIR/kernel_journal.txt

# 网络
ss -s > $OUTPUT_DIR/ss_summary.txt
ss -tanp > $OUTPUT_DIR/ss_tcp.txt
conntrack -C > $OUTPUT_DIR/conntrack_count.txt 2>/dev/null
conntrack -S > $OUTPUT_DIR/conntrack_stats.txt 2>/dev/null
netstat -s > $OUTPUT_DIR/netstat_stats.txt

# 磁盘 I/O
iostat -xz 1 3 > $OUTPUT_DIR/iostat.txt
df -h > $OUTPUT_DIR/df.txt
mount > $OUTPUT_DIR/mount.txt

# K8s 相关
crictl ps -a > $OUTPUT_DIR/containers.txt 2>/dev/null
crictl stats > $OUTPUT_DIR/container_stats.txt 2>/dev/null
journalctl -u kubelet --since "10 minutes ago" > $OUTPUT_DIR/kubelet_log.txt

# Cgroup 信息
find /sys/fs/cgroup/kubepods.slice -name "cpu.stat" -exec sh -c \
  'echo "=== {} ===" && cat {}' \; > $OUTPUT_DIR/cgroup_cpu_stats.txt 2>/dev/null

# 打包上传
tar czf ${OUTPUT_DIR}.tar.gz -C /tmp $(basename $OUTPUT_DIR)
aws s3 cp ${OUTPUT_DIR}.tar.gz ${S3_BUCKET}/${NODE_NAME}/${TIMESTAMP}/

# 清理本地
rm -rf $OUTPUT_DIR ${OUTPUT_DIR}.tar.gz

echo "Diagnostics uploaded to ${S3_BUCKET}/${NODE_NAME}/${TIMESTAMP}/"

7.5.2 eBPF 持续剖析(Parca/Pyroscope)

YAML
# Parca Agent DaemonSet(简化版)
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: parca-agent
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: parca-agent
  template:
    metadata:
      labels:
        app: parca-agent
    spec:
      priorityClassName: system-node-critical
      hostPID: true
      containers:
      - name: parca-agent
        image: ghcr.io/parca-dev/parca-agent:v0.30.0
        args:
        - --remote-store-address=parca-server:7070
        - --sampling-rate=19  # 19Hz,极低开销
        - --debug-infod-upstream-servers=debuginfod.elfutils.org
        securityContext:
          privileged: true
        resources:
          requests:
            cpu: 50m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi
        volumeMounts:
        - name: debugfs
          mountPath: /sys/kernel/debug
      volumes:
      - name: debugfs
        hostPath:
          path: /sys/kernel/debug

价值:当故障发生时,运维人员可以直接在 Parca UI 中回溯到具体时间点,查看当时究竟是哪个函数占用了 CPU,彻底消灭"无法复现"的借口。

7.6 最佳实践 Checklist

  • [ ] Prometheus Server 禁止部署在待监控节点上,必须独立集群

  • [ ] 监控组件 QoS 必须为 Guaranteed,设置 priorityClassName: system-node-critical

  • [ ] 告警消息必须包含 runbook_url,否则 P0 告警等于噪音

  • [ ] 配置 Alertmanager 抑制规则,避免告警风暴

  • [ ] sysstat 10s 采样 + Alertmanager Webhook 自动快照

  • [ ] 部署 Parca/Pyroscope 持续剖析,保留 ≥ 7 天历史

7.7 思考题

  1. 如果 Prometheus Agent 本身因为节点 OOM 被杀死,你还有什么手段获取节点最后的状态?

  2. 告警抑制规则配置不当可能导致"静默故障"(真正的问题被抑制了)。你会如何设计防护机制?

  3. 在 Serverless K8s(如 Fargate)环境中,无法部署 DaemonSet 和 Static Pod。你会如何构建可观测性?


第八章:镜像治理与资源配额最佳实践

8.1 学习目标

完成本章学习后,你将能够:

  1. 推行 Distroless/Alpine 极简镜像,消除安全与启动性能风险

  2. 使用 Sidecar / Admission Controller / eBPF 实现无侵入可观测性

  3. 掌握 Requests/Limits 黄金比例与三大 QoS 反模式

  4. 设计镜像安全基线与准入控制策略

8.2 为什么不要在业务镜像中内置重量级工具

8.2.1 反模式的四大风险

8.2.2 Distroless 镜像实践

Dockerfile
# 多阶段构建:构建阶段使用完整工具链,运行阶段使用 Distroless
# === 构建阶段 ===
FROM golang:1.22-bookworm AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

# === 运行阶段(Distroless)===
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
# 没有 shell、没有包管理器、没有调试工具
# 攻击面最小化
USER nonroot:nonroot
ENTRYPOINT ["/server"]

镜像大小对比

8.2.3 镜像安全准入控制

YAML
# OPA Gatekeeper / Kyverno 策略:禁止包含特定工具的镜像
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: restrict-image-tools
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
  parameters:
    repos:
    - "gcr.io/distroless/"
    - "registry.internal/prod/"
---
# Kyverno 策略:禁止 latest 标签
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
  - name: require-image-tag
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "An image tag is required."
      pattern:
        spec:
          containers:
          - image: "*:*"
  - name: validate-image-tag
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "Using 'latest' tag is not allowed."
      pattern:
        spec:
          containers:
          - image: "!*:latest"

8.3 正确的可观测性预埋

8.3.1 Sidecar 模式

YAML
# 在 Pod 中注入调试 Sidecar(共享 Network Namespace)
apiVersion: v1
kind: Pod
metadata:
  name: app-with-debug-sidecar
spec:
  containers:
  # 主业务容器(Distroless,无调试工具)
  - name: app
    image: registry.internal/prod/myapp:v1.2.3
    resources:
      requests:
        cpu: "1"
        memory: "2Gi"
      limits:
        cpu: "2"
        memory: "4Gi"

  # 调试 Sidecar(按需启用,平时可设 replicas=0)
  - name: debug
    image: nicolaka/netshoot:latest
    command: ["sleep", "infinity"]
    securityContext:
      capabilities:
        add: ["NET_ADMIN", "NET_RAW", "SYS_PTRACE"]
    resources:
      requests:
        cpu: 10m
        memory: 32Mi
      limits:
        cpu: 100m
        memory: 128Mi
Bash
# 需要抓包时,进入 Sidecar(与主容器共享网络栈)
kubectl exec -it app-with-debug-sidecar -c debug -- tcpdump -i eth0 -w /tmp/capture.pcap

# 需要 DNS 诊断时
kubectl exec -it app-with-debug-sidecar -c debug -- dig service-a.default.svc.cluster.local

# 需要查看连接状态时
kubectl exec -it app-with-debug-sidecar -c debug -- ss -tanp

8.3.2 OpenTelemetry Operator 自动注入

YAML
# 安装 OTel Operator 后,通过 annotation 自动注入 SDK
apiVersion: v1
kind: Pod
metadata:
  name: java-app
  annotations:
    instrumentation.opentelemetry.io/inject-java: "true"  # 自动注入 Java Agent
spec:
  containers:
  - name: app
    image: registry.internal/prod/java-app:v2.0
    # 无需修改代码,OTel Operator 自动注入 javaagent

8.3.3 eBPF 节点级无侵入采集

YAML
# Grafana Beyla / Cilium Hubble:节点级 eBPF 采集
# 无需修改应用代码,无需 Sidecar,直接在节点层面采集 HTTP/gRPC 指标
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: beyla
  namespace: monitoring
spec:
  template:
    spec:
      hostPID: true
      containers:
      - name: beyla
        image: grafana/beyla:1.5
        securityContext:
          privileged: true
        env:
        - name: BEYLA_OPEN_PORT
          value: "8080,8443,9090"  # 自动发现监听这些端口的进程
        - name: BEYLA_PROMETHEUS_PORT
          value: "9090"

8.4 Requests/Limits 黄金比例

8.4.1 本质区别

Plain
Requests(请求):
├─ 调度依据:kube-scheduler 根据它决定 Pod 放在哪个节点
├─ QoS 基准:决定 Pod 的 QoS 等级
└─ 资源预留:节点为该 Pod 预留的资源量

Limits(限制):
├─ 内核硬限制:对应 cgroups 的 cpu.max / memory.max
├─ CPU 超限:CFS Throttling(挂起,不杀)
└─ 内存超限:OOM Kill(直接杀死)

8.4.2 三大 QoS 配置模板

YAML
# === Guaranteed(核心服务:数据库、网关、关键微服务)===
resources:
  requests:
    cpu: "4"
    memory: "8Gi"
  limits:
    cpu: "4"        # 必须等于 requests
    memory: "8Gi"   # 必须等于 requests
# 特点:最高存活优先级,最后被驱逐
# 适用:延迟敏感、不可中断的核心服务

---
# === Burstable(普通业务服务)===
resources:
  requests:
    cpu: "1"        # Requests = Limits 的 50%-70%
    memory: "2Gi"
  limits:
    cpu: "2"
    memory: "4Gi"
# 特点:允许突发使用空闲资源,资源紧张时被限制
# 适用:后端业务逻辑、非核心 Web 服务

---
# === BestEffort(批处理/离线任务)===
resources: {}       # 不设置任何 Requests/Limits
# 特点:利用碎片资源,最先被驱逐
# 适用:日志清洗、CI/CD 构建、离线计算

8.4.3 Java 应用特殊配置

YAML
# Java 应用资源配置
resources:
  requests:
    cpu: "2"
    memory: "3Gi"     # Heap(2G) × 1.5 = 3G
  limits:
    cpu: "4"          # 允许 GC 突发
    memory: "3Gi"     # 必须 ≥ Heap × 1.5
---
# JVM 参数(与资源配置匹配)
env:
- name: JAVA_OPTS
  value: >-
    -Xms2g -Xmx2g
    -XX:MaxRAMPercentage=75.0
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=200
    -XX:ActiveProcessorCount=4    # 必须与 CPU Limit 匹配!
    -XX:+UseContainerSupport

关键公式

Plain
Memory Limit ≥ Xmx × 1.5
             = Heap + Metaspace(~256MB) + 线程栈(线程数×1MB) + Native + 安全余量

示例:Xmx=2G → Limit ≥ 3G

8.4.4 反模式警示

8.5 实战 Lab

Lab 8.1:镜像瘦身对比

Bash
# 构建臃肿镜像
cat > Dockerfile.fat << 'EOF'
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    openjdk-17-jdk vim curl tcpdump net-tools strace htop
COPY app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]
EOF
docker build -f Dockerfile.fat -t myapp:fat .
docker images myapp:fat
# SIZE: ~850MB

# 构建 Distroless 镜像
cat > Dockerfile.slim << 'EOF'
FROM eclipse-temurin:17-jre-alpine AS builder
COPY app.jar /app/app.jar

FROM gcr.io/distroless/java17-debian12:nonroot
COPY --from=builder /app/app.jar /app/app.jar
USER nonroot
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
EOF
docker build -f Dockerfile.slim -t myapp:slim .
docker images myapp:slim
# SIZE: ~220MB

# 对比 Pod 启动时间
time kubectl run fat-test --image=myapp:fat --restart=Never
time kubectl run slim-test --image=myapp:slim --restart=Never

Lab 8.2:Burstable Pod 在节点高负载下的降级

Bash
# 步骤1:部署 Burstable Pod(Requests=0.5, Limits=2)
# 步骤2:在节点上施加 CPU 压力
stress-ng --cpu $(nproc) --timeout 120s &

# 步骤3:观察 Burstable Pod 的 CPU 使用
kubectl top pod burstable-test
# CPU 使用应被限制在 Requests 附近(0.5 核)

# 步骤4:查看 CFS 统计
cat /sys/fs/cgroup/kubepods.slice/.../cpu.stat
# nr_throttled 应 > 0

# 步骤5:压力解除后,Pod 应恢复到 Limits 附近

8.6 最佳实践 Checklist

  • [ ] 生产镜像禁止包含 shell、包管理器、网络诊断工具

  • [ ] 使用多阶段构建,运行阶段基于 Distroless 或 Alpine

  • [ ] 所有镜像必须使用固定版本标签,禁止 latest

  • [ ] 部署 OPA/Kyverno 准入控制,强制镜像仓库白名单

  • [ ] Java 应用 Memory Limit ≥ Heap × 1.5

  • [ ] 延迟敏感型服务优先不设 CPU Limit,仅通过 Requests 预留

  • [ ] 所有 Pod 必须显式声明 Requests,禁止"只设 Limits 不设 Requests"

  • [ ] 基于 Prometheus 历史数据(P95)设置 Requests,而非拍脑袋

8.7 思考题

  1. 如果一个 Go 应用使用 CGO 调用了 C 库,能否使用 distroless/static 镜像?应该选择哪个 Distroless 变体?

  2. 在 Spot/抢占式实例上运行的 Pod,QoS 等级对存活率有什么影响?你会如何设计?

  3. 如何自动化地检测集群中所有"只设 Limits 不设 Requests"的 Pod?


第九章:生产内核基线与 K8s 配置规范

9.1 学习目标

完成本章学习后,你将能够:

  1. 制定并验证 Linux 内核 sysctl 基线(网络/内存/文件系统)

  2. 配置 Kubelet 关键参数的生产级模板

  3. 选型并调优 CNI 与容器运行时(containerd),掌握 iptables → IPVS → eBPF 的演进路径

  4. 建立基线审计与合规检查机制,实现配置即代码(Configuration as Code)的 GitOps 管理


9.2 Linux 内核 sysctl 基线(补齐节标题与引导段)

引导说明

Linux 内核参数是 K8s 节点性能的"地基"。错误的 sysctl 配置可能导致:

  • conntrack 表溢出 → 网络丢包(第二章)

  • inotify 上限不足 → kubelet 无法监控容器变化

  • swappiness 过高 → 内存压力时 Pod 被换出导致延迟飙升

  • 文件描述符不足 → 大连接数场景下 accept 失败

本节提供经过生产验证的 sysctl 基线模板,按网络、内存、文件系统三个维度组织,每个参数均附带注释说明其作用与推荐依据。

部署原则

  • 所有配置通过 /etc/sysctl.d/99-k8s-*.conf 文件管理,禁止使用 sysctl -w 临时修改

  • 配置变更必须通过 GitOps 流水线(Ansible/ArgoCD)下发,禁止手动 SSH 修改

  • 每次变更后执行验证脚本(9.2.4),确认生效且无副作用


9.2.1 网络参数(补齐缺失的开头部分)

Bash
# /etc/sysctl.d/99-k8s-network.conf
# K8s 节点网络内核参数基线
# 适用:Linux Kernel 5.15+,K8s 1.28+
# 最后更新:2025-06

# === Conntrack(连接追踪)===
# K8s 每个 Service 连接都会被 conntrack 追踪
# 默认值 262144 在 >100 Pod 的节点上极易溢出
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_buckets = 262144
# 缩短 TCP 超时,加速 conntrack 表项回收
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
# UDP 超时(DNS 相关,默认 30s 已合理)
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 120

# === TCP 优化 ===
net.ipv4.tcp_tw_reuse = 1                    # 允许重用 TIME_WAIT 连接(出站)
net.ipv4.tcp_fin_timeout = 15                # 缩短 FIN_WAIT2 超时(默认60s)
net.ipv4.tcp_max_tw_buckets = 65536          # TIME_WAIT 上限(超出直接 RST)
net.ipv4.tcp_syncookies = 1                  # 防 SYN Flood 攻击
net.ipv4.tcp_max_syn_backlog = 65535         # SYN 半连接队列大小
net.core.somaxconn = 65535                   # listen 全连接队列大小
net.ipv4.tcp_rmem = 4096 87380 16777216      # TCP 接收缓冲区(min/default/max)
net.ipv4.tcp_wmem = 4096 65536 16777216      # TCP 发送缓冲区(min/default/max)
net.ipv4.tcp_mtu_probing = 1                 # 启用 MTU 探测(避免黑洞路由)
net.ipv4.tcp_slow_start_after_idle = 0       # 禁用空闲后慢启动(长连接场景)
net.ipv4.tcp_no_metrics_save = 1             # 不缓存 TCP 连接指标

# === 网络核心 ===
net.core.netdev_max_backlog = 65536          # 网卡接收队列长度(默认1000)
net.core.rmem_max = 16777216                 # 接收缓冲区上限
net.core.wmem_max = 16777216                 # 发送缓冲区上限
net.core.default_qdisc = fq                  # 公平队列调度器(配合 tcp_bbr)
net.core.optmem_max = 65536                  # 辅助缓冲区上限
net.ipv4.ip_local_port_range = 1024 65535    # 本地端口范围(扩大,默认32768-60999)

# === 桥接(K8s 必需)===
# 确保桥接流量经过 iptables/nftables 处理
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1

# === ARP 优化(大集群)===
# 默认 gc_thresh 过小,大集群中 ARP 表频繁回收导致网络抖动
net.ipv4.neigh.default.gc_thresh1 = 4096
net.ipv4.neigh.default.gc_thresh2 = 8192
net.ipv4.neigh.default.gc_thresh3 = 16384
net.ipv4.neigh.default.gc_stale_time = 120

# === IPv6(如不使用则禁用,减少攻击面)===
# net.ipv6.conf.all.disable_ipv6 = 1
# net.ipv6.conf.default.disable_ipv6 = 1

参数选择依据


9.3.2 关键参数说明(补齐缺失表格)


9.4.1 选型对比(补齐缺失表格)

选型决策树

Plain
是否需要 L7 NetworkPolicy(HTTP/gRPC 级别)?
├─ 是 → Cilium(唯一原生支持)
└─ 否 → 集群规模?
         ├─ < 100 节点 → Flannel(简单够用)或 Calico BGP
         ├─ 100-1000 节点 → Calico BGP/eBPF
         └─ > 1000 节点 → Cilium 或 Calico eBPF
              └─ 是否需要替代 kube-proxy?
                   ├─ 是 → Cilium(kube-proxy-replacement: strict)
                   └─ 否 → Calico eBPF + IPVS

性能压测基准(选型前必做):

Bash
# 使用 netperf 测试 CNI 网络性能
# 1. 吞吐量(TCP_STREAM)
netperf -H <pod-ip> -t TCP_STREAM -l 30 -- -m 1400

# 2. 延迟(TCP_RR)
netperf -H <pod-ip> -t TCP_RR -l 30 -- -r 1,1

# 3. PPS(UDP_STREAM,小包)
netperf -H <pod-ip> -t UDP_STREAM -l 30 -- -m 64

# 4. Service 规则规模测试
# 创建 5000 个 Service,观察 iptables/ipvs/eBPF 规则同步延迟
for i in $(seq 1 5000); do
  kubectl create service clusterip svc-$i --tcp=80:80 &
done

# 5. 对比指标
# - 规则同步延迟(从 Service 创建到 Pod 可访问)
# - 首包延迟(新连接建立时间)
# - CPU 开销(kube-proxy / cilium-agent 的 CPU 使用率)

9.9 实战 Lab(新增)

Lab 9.1:sysctl 基线部署与验证

Bash
# 步骤1:部署基线配置文件
cat > /etc/sysctl.d/99-k8s-network.conf << 'EOF'
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_buckets = 262144
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 65536
net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.ipv4.neigh.default.gc_thresh1 = 4096
net.ipv4.neigh.default.gc_thresh2 = 8192
net.ipv4.neigh.default.gc_thresh3 = 16384
EOF

cat > /etc/sysctl.d/99-k8s-memory.conf << 'EOF'
vm.swappiness = 1
vm.min_free_kbytes = 1048576
vm.overcommit_memory = 1
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.vfs_cache_pressure = 200
vm.zone_reclaim_mode = 0
EOF

cat > /etc/sysctl.d/99-k8s-fs.conf << 'EOF'
fs.inotify.max_user_watches = 1048576
fs.inotify.max_user_instances = 8192
fs.file-max = 2097152
fs.nr_open = 2097152
kernel.pid_max = 4194304
kernel.panic = 10
kernel.panic_on_oops = 1
kernel.hung_task_timeout_secs = 120
EOF

# 步骤2:应用配置
sysctl --system

# 步骤3:运行验证脚本
bash verify-sysctl-baseline.sh
# 预期输出:All sysctl checks passed

# 步骤4:验证 K8s 功能不受影响
kubectl get nodes
kubectl run test-net --image=busybox --restart=Never -- wget -qO- http://kubernetes.default.svc/healthz
kubectl delete pod test-net

Lab 9.2:Kubelet 配置灰度变更

Bash
# 步骤1:选择 1 个测试节点
TEST_NODE="node-worker-01"

# 步骤2:备份当前配置
ssh $TEST_NODE "cp /var/lib/kubelet/config.yaml /var/lib/kubelet/config.yaml.bak.$(date +%Y%m%d)"

# 步骤3:修改配置(示例:调整日志轮转)
ssh $TEST_NODE "sed -i 's/containerLogMaxSize:.*/containerLogMaxSize: \"100Mi\"/' /var/lib/kubelet/config.yaml"
ssh $TEST_NODE "sed -i 's/containerLogMaxFiles:.*/containerLogMaxFiles: 5/' /var/lib/kubelet/config.yaml"

# 步骤4:重启 kubelet
ssh $TEST_NODE "systemctl restart kubelet"

# 步骤5:验证节点状态
kubectl get node $TEST_NODE
# 应为 Ready

# 步骤6:验证 Pod 正常运行
kubectl get pods --field-selector spec.nodeName=$TEST_NODE
# 所有 Pod 应为 Running

# 步骤7:观察 30 分钟,确认无异常后推广到 10% 节点
# 步骤8:全量推广

Lab 9.3:CNI 性能对比测试

Bash
# 步骤1:部署 netperf 测试 Pod
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: netperf-server
  labels:
    app: netperf
spec:
  containers:
  - name: netperf
    image: networkstatic/netperf
    command: ["netserver", "-D"]
    ports:
    - containerPort: 12865
---
apiVersion: v1
kind: Pod
metadata:
  name: netperf-client
spec:
  containers:
  - name: netperf
    image: networkstatic/netperf
    command: ["sleep", "3600"]
EOF

# 步骤2:等待 Pod Ready
kubectl wait --for=condition=Ready pod/netperf-server --timeout=60s
kubectl wait --for=condition=Ready pod/netperf-client --timeout=60s

# 步骤3:获取 Server Pod IP
SERVER_IP=$(kubectl get pod netperf-server -o jsonpath='{.status.podIP}')

# 步骤4:TCP 吞吐量测试
kubectl exec netperf-client -- netperf -H $SERVER_IP -t TCP_STREAM -l 30
# 记录 Throughput(Mbps)

# 步骤5:TCP 延迟测试
kubectl exec netperf-client -- netperf -H $SERVER_IP -t TCP_RR -l 30
# 记录 Transaction Rate(trans/s)

# 步骤6:UDP PPS 测试
kubectl exec netperf-client -- netperf -H $SERVER_IP -t UDP_STREAM -l 30 -- -m 64
# 记录 Throughput(10^6bits/s)

# 步骤7:跨节点测试(将 client 调度到不同节点)
# 对比同节点 vs 跨节点性能差异

# 步骤8:清理
kubectl delete pod netperf-server netperf-client

9.10 常见误区(新增)


9.11 本章小结(新增)

本章将前八章中零散出现的调优参数系统化为可审计、可落地、可追溯的生产基线:

Plain
┌─────────────────────────────────────────────────────────┐
│                  第9章知识体系                            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  内核层(sysctl)                                        │
│  ├─ 网络:conntrack / TCP / 桥接 / ARP                  │
│  ├─ 内存:swappiness / dirty / NUMA / OOM              │
│  └─ 文件系统:inotify / fd / pid / panic               │
│                                                         │
│  K8s 层(Kubelet)                                      │
│  ├─ 资源预留与驱逐                                      │
│  ├─ 镜像管理与日志轮转                                  │
│  ├─ CPU/内存/拓扑管理器                                 │
│  └─ 健康检查与认证授权                                  │
│                                                         │
│  网络层(CNI + kube-proxy)                             │
│  ├─ 选型:Flannel → Calico → Cilium                    │
│  ├─ 演进:iptables → IPVS → eBPF                      │
│  └─ 调优:conntrack 表 / 规则同步 / Hubble             │
│                                                         │
│  运行时层(containerd)                                  │
│  ├─ SystemdCgroup 一致性                               │
│  ├─ 镜像拉取并发控制                                    │
│  └─ shim 进程管理                                      │
│                                                         │
│  治理层(审计与变更)                                    │
│  ├─ 自动化审计脚本                                     │
│  ├─ GitOps 配置管理(Ansible + ArgoCD)                │
│  └─ 灰度发布与回滚                                     │
│                                                         │
└─────────────────────────────────────────────────────────┘

核心原则

  • 配置即代码:所有基线通过 Git 仓库管理,变更通过 PR 审批

  • 持续合规:每季度自动审计,偏差自动告警

  • 灰度变更:任何配置变更遵循 1 节点 → 10% → 全量的发布节奏

  • 可回滚:每次变更前自动备份,保留最近 5 个版本


以上为第9章全部缺失内容的补齐。将上述内容插入原文档对应位置后,第9章结构完整,包含:学习目标 → 内核基线(网络/内存/文件系统)→ Kubelet 配置 → CNI 选型 → 运行时调优 → 基线审计 → Checklist → 思考题 → 实战 Lab → 常见误区 → 本章小结。

第十章:综合实战——全链路故障复盘工作坊

10.1 学习目标

完成本章学习后,你将能够:

  1. 独立完成从告警→止血→取证→根因→修复→预防的全流程

  2. 撰写符合 SRE 标准的 RCA(Root Cause Analysis)报告

  3. 将个案转化为 Checklist / 告警规则 / 自愈脚本 / 基线变更

  4. 组织高效的故障复盘会议(Blameless Postmortem)

10.2 Case Study A:NVMe 驱动 Bug 导致节点假死

10.2.1 故障现象

Plain
时间:2025-06-09 03:22 UTC
告警:
  [P1] NodeCPULoadHigh - node-worker-07 load5=210 (96核)
  [P1] NodeDiskIOSaturation - node-worker-07 %util=100%
  [P0] NodeNotReady - node-worker-07 (5分钟后)
  [P0] PodEviction - 47 Pods evicted from node-worker-07

业务影响:
  - 3 个核心微服务 P99 延迟从 5ms 飙升到 2000ms
  - 订单服务错误率从 0.01% 升至 12%
  - 持续 23 分钟

10.2.2 排查时间线

Plain
03:22:15  Alertmanager 触发 NodeCPULoadHigh
03:22:30  值班 SRE 收到 P1 告警,SSH 登录节点
03:23:00  SSH 响应极慢(>30s),top 显示 Load=210,CPU Usage=35%
03:23:15  执行 ps -eo stat | grep -c D → 输出:89(89个D-State进程!)
03:23:30  iostat -xz 1 → nvme0n1 %util=100%, await=850ms
03:24:00  dmesg -T | grep -i nvme → "nvme nvme0: I/O 45678 timeout"
03:24:30  判断:NVMe 驱动 I/O 超时导致 D-State 堆积
03:25:00  止血:kubectl cordon node-worker-07
03:25:30  kubectl drain node-worker-07 --timeout=60s --force
03:26:00  网关限流:订单服务 RPS 从 5000 降至 2000
03:28:00  业务 P99 开始回落
03:35:00  业务完全恢复
03:40:00  节点仍无响应,通过 IPMI SOL 获取 Serial Console 日志
03:42:00  确认内核日志:"nvme nvme0: controller is down; will reset"
04:00:00  云厂商确认:NVMe 固件 Bug(特定批次)

10.2.3 根因分析(5 Whys)

Plain
Why 1: 为什么节点假死?
→ 89 个进程处于 D-State,等待 NVMe I/O 完成

Why 2: 为什么 NVMe I/O 超时?
→ NVMe 控制器固件 Bug,在高 IOPS 下触发内部死锁

Why 3: 为什么高 IOPS?
→ 节点上运行了 12 个 Java 应用,GC 日志 + 应用日志写入峰值达 80K IOPS

Why 4: 为什么日志写入如此集中?
→ 未配置日志轮转,且 3 个应用使用了同步日志框架(Log4j SyncAppender)

Why 5: 为什么没有提前发现?
→ 缺少 NVMe 健康监控(SMART 数据)和 IOPS 趋势告警

10.2.4 修复与预防

10.3 Case Study B:CFS Throttling 导致 Java Pod 循环重启

10.3.1 故障现象

Plain
时间:2025-07-15 14:30 UTC
告警:
  [P1] PodCrashLooping - payment-service-7d8f9 (RestartCount=15)
  [P2] ContainerCPUThrottling - payment-service throttled_ratio=35%

业务影响:
  - 支付服务可用性从 99.99% 降至 97.5%
  - 每次重启导致 30s 不可用窗口
  - 持续 2 小时(直到人工介入)

10.3.2 排查过程

Bash
# 步骤1:查看 Pod 事件
kubectl describe pod payment-service-7d8f9
# Events:
#   Warning  Unhealthy  2m  kubelet  Liveness probe failed: HTTP probe failed: context deadline exceeded
#   Normal   Killing    2m  kubelet  Container failed liveness probe, will be restarted

# 步骤2:查看 CFS 限流统计
POD_UID=$(kubectl get pod payment-service-7d8f9 -o jsonpath='{.metadata.uid}')
cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable/kubepods-burstable-pod${POD_UID}.slice/cpu.stat
# nr_periods: 45230
# nr_throttled: 15830    ← 35% 的周期被限流!
# throttled_usec: 89200000  ← 累计被限流 89.2 秒

# 步骤3:查看 GC 日志
kubectl logs payment-service-7d8f9 --previous | grep "pause"
# [GC pause (G1 Evacuation Pause) (young), 0.8234567 secs]  ← 正常应 < 0.1s
# [GC pause (G1 Humongous Allocation), 1.2345678 secs]      ← 严重超时!

# 步骤4:Off-CPU 火焰图验证
PID=$(crictl inspect $(crictl ps --name payment -q) | jq '.info.pid')
/usr/share/bcc/tools/offcputime -df -p $PID 10 > offcpu.stacks
# 火焰图显示:大量时间在 schedule() → 被 CFS 挂起

# 步骤5:确认资源配置
kubectl get pod payment-service-7d8f9 -o jsonpath='{.spec.containers[0].resources}'
# {"limits":{"cpu":"1","memory":"2Gi"},"requests":{"cpu":"500m","memory":"1Gi"}}
# CPU Limit=1,但 Java 应用 + GC 线程需要突发到 2+ 核

10.3.3 根因

Java 应用 CPU Limit=1 核,但 G1 GC 在突发流量下需要额外 CPU 时间。CFS 在配额用完后挂起所有线程(包括 GC 线程),导致 GC STW 时间从 50ms 延长到 800ms+,超过 Liveness Probe 超时(1s),Pod 被 Kill 重启,形成循环。

10.3.4 修复

YAML
# 修复后的资源配置
resources:
  requests:
    cpu: "1"
    memory: "3Gi"      # Heap(2G) × 1.5
  limits:
    cpu: "4"           # 允许 GC 突发(4倍 Requests)
    memory: "3Gi"
---
# JVM 参数调整
env:
- name: JAVA_OPTS
  value: >-
    -Xms2g -Xmx2g
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=100
    -XX:ActiveProcessorCount=4
    -XX:+UseContainerSupport
    -XX:+ParallelRefProcEnabled
---
# Liveness Probe 调整
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 60    # 从 30s 增加到 60s
  periodSeconds: 15          # 从 10s 增加到 15s
  timeoutSeconds: 5          # 从 1s 增加到 5s
  failureThreshold: 5        # 从 3 增加到 5

10.4 Case Study C:CoreDNS UDP conntrack 冲突导致微服务超时

10.4.1 故障现象

Plain
时间:2025-08-20 09:15 UTC
告警:
  [P2] ServiceLatencyHigh - 多个微服务 P99 > 5s(偶发)
  [P2] DNSResolutionSlow - gethostlatency avg=250ms

业务影响:
  - 所有微服务偶发 5s 超时(约 2% 请求)
  - 无规律,难以复现
  - 持续 3 天(被误判为"网络抖动")

10.4.2 排查过程

Bash
# 步骤1:eBPF 确认 DNS 延迟
/usr/share/bcc/tools/gethostlatency
# TIME      PID    COMM         LATms HOST
# 09:15:23  12345  java         0.03  service-a.ns.svc.cluster.local
# 09:15:24  12345  java       253.10  service-b.ns.svc.cluster.local  ← 偶发高延迟
# 09:15:25  12678  python       0.02  service-a.ns.svc.cluster.local

# 步骤2:排除 CoreDNS 资源问题
kubectl top pods -n kube-system -l k8s-app=kube-dns
# CPU/Memory 均正常(< 50%)

# 步骤3:检查 conntrack UDP 冲突(关键!)
conntrack -S
# cpu=0 found=0 invalid=0 insert=45230 insert_failed=8912 ← 关键!
# drop=0 early_drop=0
# insert_failed=8912 → UDP conntrack 插入冲突!

# 步骤4:理解根因
# Linux conntrack 对 UDP 的处理:
# 当两个并发的 DNS 查询(相同 src_ip:src_port → 53)到达时,
# 第二个包的 conntrack 插入会失败(insert_failed++),
# 导致该 DNS 响应包被丢弃,应用等待超时后重试

# 步骤5:验证
# 在高并发节点上抓包
tcpdump -i eth0 port 53 -nn -c 100
# 观察是否有 DNS 查询发出但无响应

10.4.3 根因

Linux 内核 conntrack 模块在处理并发 UDP 连接时存在已知的竞态条件(race condition)。当多个 Pod 同时发起 DNS 查询,且源端口恰好相同时,第二个 conntrack 条目插入失败,DNS 响应包被丢弃。应用等待 5 秒超时后重试才成功。

10.4.4 修复

YAML
# 方案一:部署 NodeLocal DNSCache(推荐)
# 每个节点运行一个本地 DNS 缓存,使用 TCP 连接 CoreDNS(避免 UDP 冲突)
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-local-dns
  namespace: kube-system
spec:
  template:
    spec:
      containers:
      - name: node-cache
        image: registry.k8s.io/dns/k8s-dns-node-cache:1.22.28
        args:
        - "-localip"
        - "169.254.20.10"
        - "-conf"
        - "/etc/Corefile"
        - "-upstreamsvc"
        - "kube-dns"
        securityContext:
          privileged: true
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
---
# 方案二:Pod DNS 配置优化
# 减少不必要的 DNS 查询
spec:
  dnsConfig:
    options:
    - name: ndots
      value: "2"           # 从默认 5 降为 2(减少搜索域查询次数)
    - name: single-request-reopen
      # 强制 A 和 AAAA 查询使用不同 socket(避免 conntrack 冲突)
    - name: timeout
      value: "1"           # 超时从 5s 降为 1s(快速重试)
    - name: attempts
      value: "3"

10.5 RCA 报告标准模板

Markdown
# RCA 报告:[故障标题]

## 元信息
| 项目 | 内容 |
|------|------|
| 报告编号 | RCA-2025-0609-001 |
| 严重等级 | P0 / P1 / P2 |
| 影响时长 | XX 分钟 |
| 影响范围 | XX 服务 / XX% 用户 |
| 值班人员 | @name |
| 报告撰写 | @name |
| 复盘日期 | YYYY-MM-DD |

## 1. 故障摘要(Executive Summary)
> 一段话概括:什么时间、什么系统、发生了什么、影响了什么、如何恢复的。

## 2. 影响评估
- 业务影响:[量化 RED 指标变化]
- 用户影响:[受影响用户数/比例]
- 财务影响:[如有]
- SLA 影响:[是否违反 SLA]

## 3. 时间线(精确到秒)
| 时间(UTC) | 事件 | 操作人 |
|-----------|------|--------|
| 03:22:15 | Alertmanager 触发 NodeCPULoadHigh | 系统 |
| 03:22:30 | SRE 收到告警,开始排查 | @name |
| ... | ... | ... |

## 4. 止血动作与有效性验证
- 止血动作:[具体命令/操作]
- 验证指标:
  - [ ] 热点核心利用率 ≤ 75%
  - [ ] 业务 RT 回落到基线
  - [ ] 错误率恢复正常

## 5. 排查路径
### 5.1 初始假设与排除
| 假设 | 验证方法 | 结果 | 排除/确认 |
|------|----------|------|-----------|
| CPU 不足 | mpstat | Usage=35% | 排除 |
| 网络故障 | ping/traceroute | 正常 | 排除 |
| 存储故障 | iostat | %util=100% | 确认 |

### 5.2 根因定位过程
[详细排查步骤、使用的命令、观察到的数据]

## 6. 根因(5 Whys)
[逐层追问,直到根本原因]

## 7. 修复措施
| 类别 | 措施 | 负责人 | 截止日期 | 状态 |
|------|------|--------|----------|------|
| 临时 | ... | ... | ... | ✅ |
| 短期 | ... | ... | ... | 🔄 |
| 长期 | ... | ... | ... | ⏳ |

## 8. 预防项
- [ ] 新增告警规则:[PromQL]
- [ ] 新增 NPD 检测规则:[pattern]
- [ ] 更新基线配置:[参数]
- [ ] 更新 Runbook:[链接]
- [ ] 新增自愈脚本:[路径]
- [ ] 培训计划:[内容/日期]

## 9. 经验教训
### 做得好的
- ...
### 需要改进的
- ...
### 运气成分
- ...

## 10. 附件
- 监控截图
- 火焰图
- 内核日志
- 相关 PR/变更链接

10.6 Blameless Postmortem 会议指南

10.6.1 会议原则

  1. 对事不对人:关注系统和流程缺陷,而非个人失误

  2. 假设善意:每个人都在当时信息下做了最合理的决策

  3. 鼓励透明:只有坦诚才能发现真正的系统缺陷

  4. 产出导向:每个 Action Item 必须有负责人和截止日期

10.6.2 会议议程(60 分钟)

10.6.3 从个案到体系

Plain
个案故障

RCA 报告

┌─────────────────────────────────────────┐
│  转化为体系化预防措施                     │
├─────────────────────────────────────────┤
│  → 新增/修改告警规则(Prometheus)        │
│  → 新增 NPD 检测模式                     │
│  → 更新节点基线配置(sysctl/kubelet)     │
│  → 新增自愈脚本(Ansible Playbook)       │
│  → 更新 Runbook(排查 SOP)              │
│  → 新增 Lab 实验(培训教材)              │
│  → 更新准入控制策略(OPA/Kyverno)        │
│  → 纳入 Game Day 演练场景                │
└─────────────────────────────────────────┘

10.7 结业考核

考核形式

在隔离的实验环境中,讲师注入一个复合故障(组合以下至少 2 种):

  • CFS Throttling + DNS 延迟

  • SoftIRQ 风暴 + conntrack 溢出

  • D-State I/O 阻塞 + 监控断连

  • NUMA 跨节点访问 + CPU Steal

考核标准

通过标准

  • 总分 ≥ 80 分

  • 止血速度 ≤ 5 分钟(一票否决项:> 10 分钟不通过)

  • 根因定位正确(一票否决项:根因错误不通过)


附录

附录 A:Linux 性能工具速查表

附录 B:K8s 节点排查流程图

Plain
┌──────────────┐
                        │   告警触发    │
                        └──────┬───────┘

                    ┌──────────▼──────────┐
                    │  业务是否受损?       │
                    └──────────┬──────────┘
                         │           │
                        是           否
                         │           │
              ┌──────────▼───┐       │
              │  立即止血     │       │
              │ cordon+drain │       │
              │ + 网关限流   │       │
              └──────────┬───┘       │
                         │           │
              ┌──────────▼───┐       │
              │  验证恢复     │       │
              │ 3项指标      │       │
              └──────────┬───┘       │
                         │           │
                         └─────┬─────┘

                    ┌──────────▼──────────┐
                    │  采集宏观指标         │
                    │ mpstat/iostat/ss/sar │
                    └──────────┬──────────┘

          ┌────────┬───────┬───┴────┬────────┬────────┐
          │        │       │        │        │        │
     %soft高   %wa高   %sys高   %steal高  Throttle  假死
          │        │       │        │        │        │
     IRQ/RPS   I/O排查  conntrack  云厂商   cpu.stat  Serial
     小包洪峰  D-State  iptables  独占实例  CPU Burst  kdump
     协议栈    CSI/NFS  perf top  Anti-Aff  调Limit   NPD
          │        │       │        │        │        │
          └────────┴───────┴───┬────┴────────┴────────┘

                    ┌──────────▼──────────┐
                    │  根因确认            │
                    │  eBPF/Perf 微观验证  │
                    └──────────┬──────────┘

                    ┌──────────▼──────────┐
                    │  修复 + 预防         │
                    │  基线更新/告警/自愈   │
                    └──────────┬──────────┘

                    ┌──────────▼──────────┐
                    │  RCA 报告归档        │
                    │  复盘会议            │
                    └─────────────────────┘

附录 C:生产配置模板清单

附录 D:术语表

附录 E:参考资料

书籍

  • Brendan Gregg,《Systems Performance: Enterprise and the Cloud》(2nd Edition), 2020

  • Brendan Gregg,《BPF Performance Tools》, 2019

  • Google SRE Team,《Site Reliability Engineering》, 2016

  • Google SRE Team,《The Site Reliability Workbook》, 2018

官方文档

白皮书与博客

  • CNCF Observability Whitepaper, 2024

  • Netflix Tech Blog: "Linux Performance Analysis in 60,000 Milliseconds"

  • Cloudflare Blog: "eBPF: The Future of Networking"

  • Red Hat Blog: "Understanding CPU Throttling in Kubernetes"


教材使用建议


版本:v2.0(2026-07-31) 基于:原始大纲深度重构,修复逻辑断裂、消除内容重复、补齐教材要素、强化预防体系与现代工具链 许可:内部培训使用,未经授权不得外传 维护:每季度根据内核/K8s 版本更新内容,每年进行一次全面修订


— 全书完 —